github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/controller/destroy.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package controller
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"fmt"
    10  	"io"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/juju/clock"
    15  	"github.com/juju/cmd"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/gnuflag"
    18  	"gopkg.in/juju/names.v2"
    19  
    20  	"github.com/juju/juju/api/base"
    21  	"github.com/juju/juju/api/controller"
    22  	"github.com/juju/juju/api/credentialmanager"
    23  	"github.com/juju/juju/api/storage"
    24  	"github.com/juju/juju/apiserver/params"
    25  	jujucmd "github.com/juju/juju/cmd"
    26  	"github.com/juju/juju/cmd/juju/block"
    27  	"github.com/juju/juju/cmd/modelcmd"
    28  	"github.com/juju/juju/environs"
    29  	"github.com/juju/juju/environs/config"
    30  	"github.com/juju/juju/environs/context"
    31  	"github.com/juju/juju/jujuclient"
    32  )
    33  
    34  // NewDestroyCommand returns a command to destroy a controller.
    35  func NewDestroyCommand() cmd.Command {
    36  	cmd := destroyCommand{}
    37  	cmd.controllerCredentialAPIFunc = cmd.credentialAPIForControllerModel
    38  	cmd.environsDestroy = environs.Destroy
    39  	// Even though this command is all about destroying a controller we end up
    40  	// needing environment endpoints so we can fall back to the client destroy
    41  	// environment method. This shouldn't really matter in practice as the
    42  	// user trying to take down the controller will need to have access to the
    43  	// controller environment anyway.
    44  	return modelcmd.WrapController(
    45  		&cmd,
    46  		modelcmd.WrapControllerSkipControllerFlags,
    47  		modelcmd.WrapControllerSkipDefaultController,
    48  	)
    49  }
    50  
    51  // destroyCommand destroys the specified controller.
    52  type destroyCommand struct {
    53  	destroyCommandBase
    54  	storageAPI     storageAPI
    55  	destroyModels  bool
    56  	destroyStorage bool
    57  	releaseStorage bool
    58  }
    59  
    60  // usageDetails has backticks which we want to keep for markdown processing.
    61  // TODO(cheryl): Do we want the usage, options, examples, and see also text in
    62  // backticks for markdown?
    63  var usageDetails = `
    64  All models (initial model plus all workload/hosted) associated with the
    65  controller will first need to be destroyed, either in advance, or by
    66  specifying `[1:] + "`--destroy-all-models`." + `
    67  
    68  If there is persistent storage in any of the models managed by the
    69  controller, then you must choose to either destroy or release the
    70  storage, using ` + "`--destroy-storage` or `--release-storage` respectively." + `
    71  
    72  Examples:
    73      # Destroy the controller and all hosted models. If there is
    74      # persistent storage remaining in any of the models, then
    75      # this will prompt you to choose to either destroy or release
    76      # the storage.
    77      juju destroy-controller --destroy-all-models mycontroller
    78  
    79      # Destroy the controller and all hosted models, destroying
    80      # any remaining persistent storage.
    81      juju destroy-controller --destroy-all-models --destroy-storage
    82  
    83      # Destroy the controller and all hosted models, releasing
    84      # any remaining persistent storage from Juju's control.
    85      juju destroy-controller --destroy-all-models --release-storage
    86  
    87  See also:
    88      kill-controller
    89      unregister`
    90  
    91  var usageSummary = `
    92  Destroys a controller.`[1:]
    93  
    94  var destroySysMsg = `
    95  WARNING! This command will destroy the %q controller.
    96  This includes all machines, applications, data and other resources.
    97  
    98  Continue? (y/N):`[1:]
    99  
   100  // destroyControllerAPI defines the methods on the controller API endpoint
   101  // that the destroy command calls.
   102  type destroyControllerAPI interface {
   103  	Close() error
   104  	BestAPIVersion() int
   105  	ModelConfig() (map[string]interface{}, error)
   106  	HostedModelConfigs() ([]controller.HostedConfig, error)
   107  	CloudSpec(names.ModelTag) (environs.CloudSpec, error)
   108  	DestroyController(controller.DestroyControllerParams) error
   109  	ListBlockedModels() ([]params.ModelBlockInfo, error)
   110  	ModelStatus(models ...names.ModelTag) ([]base.ModelStatus, error)
   111  	AllModels() ([]base.UserModel, error)
   112  }
   113  
   114  type storageAPI interface {
   115  	Close() error
   116  	ListStorageDetails() ([]params.StorageDetails, error)
   117  }
   118  
   119  // destroyClientAPI defines the methods on the client API endpoint that the
   120  // destroy command might call.
   121  type destroyClientAPI interface {
   122  	Close() error
   123  	ModelGet() (map[string]interface{}, error)
   124  	DestroyModel() error
   125  }
   126  
   127  // Info implements Command.Info.
   128  func (c *destroyCommand) Info() *cmd.Info {
   129  	return jujucmd.Info(&cmd.Info{
   130  		Name:    "destroy-controller",
   131  		Args:    "<controller name>",
   132  		Purpose: usageSummary,
   133  		Doc:     usageDetails,
   134  	})
   135  }
   136  
   137  // SetFlags implements Command.SetFlags.
   138  func (c *destroyCommand) SetFlags(f *gnuflag.FlagSet) {
   139  	c.destroyCommandBase.SetFlags(f)
   140  	f.BoolVar(&c.destroyModels, "destroy-all-models", false, "Destroy all hosted models in the controller")
   141  	f.BoolVar(&c.destroyStorage, "destroy-storage", false, "Destroy all storage instances managed by the controller")
   142  	f.BoolVar(&c.releaseStorage, "release-storage", false, "Release all storage instances from management of the controller, without destroying them")
   143  }
   144  
   145  // Init implements Command.Init.
   146  func (c *destroyCommand) Init(args []string) error {
   147  	if c.destroyStorage && c.releaseStorage {
   148  		return errors.New("--destroy-storage and --release-storage cannot both be specified")
   149  	}
   150  	return c.destroyCommandBase.Init(args)
   151  }
   152  
   153  // Run implements Command.Run
   154  func (c *destroyCommand) Run(ctx *cmd.Context) error {
   155  	controllerName, err := c.ControllerName()
   156  	if err != nil {
   157  		return errors.Trace(err)
   158  	}
   159  	store := c.ClientStore()
   160  	if !c.assumeYes {
   161  		if err := confirmDestruction(ctx, controllerName); err != nil {
   162  			return err
   163  		}
   164  	}
   165  
   166  	// Attempt to connect to the API.  If we can't, fail the destroy.  Users will
   167  	// need to use the controller kill command if we can't connect.
   168  	api, err := c.getControllerAPI()
   169  	if err != nil {
   170  		return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot connect to API"), ctx, nil)
   171  	}
   172  	defer api.Close()
   173  
   174  	if api.BestAPIVersion() < 4 {
   175  		// Versions before 4 support only destroying the storage,
   176  		// and will not raise an error if there is storage in the
   177  		// controller. Force the user to specify up-front.
   178  		if c.releaseStorage {
   179  			return errors.New("this juju controller only supports destroying storage")
   180  		}
   181  		if !c.destroyStorage {
   182  			models, err := api.AllModels()
   183  			if err != nil {
   184  				return errors.Trace(err)
   185  			}
   186  			var anyStorage bool
   187  			for _, model := range models {
   188  				hasStorage, err := c.modelHasStorage(model.Name)
   189  				if err != nil {
   190  					return errors.Trace(err)
   191  				}
   192  				if hasStorage {
   193  					anyStorage = true
   194  					break
   195  				}
   196  			}
   197  			if anyStorage {
   198  				return errors.Errorf(`cannot destroy controller %q
   199  
   200  Destroying this controller will destroy the storage,
   201  but you have not indicated that you want to do that.
   202  
   203  Please run the the command again with --destroy-storage
   204  to confirm that you want to destroy the storage along
   205  with the controller.
   206  
   207  If instead you want to keep the storage, you must first
   208  upgrade the controller to version 2.3 or greater.
   209  
   210  `, controllerName)
   211  			}
   212  			c.destroyStorage = true
   213  		}
   214  	}
   215  
   216  	// Obtain controller environ so we can clean up afterwards.
   217  	controllerEnviron, err := c.getControllerEnviron(ctx, store, controllerName, api)
   218  	if err != nil {
   219  		return errors.Annotate(err, "getting controller environ")
   220  	}
   221  
   222  	cloudCallCtx := cloudCallContext(c.controllerCredentialAPIFunc)
   223  
   224  	for {
   225  		// Attempt to destroy the controller.
   226  		ctx.Infof("Destroying controller")
   227  		var hasHostedModels bool
   228  		var hasPersistentStorage bool
   229  		var destroyStorage *bool
   230  		if c.destroyStorage || c.releaseStorage {
   231  			// Set destroyStorage to true or false, if
   232  			// --destroy-storage or --release-storage
   233  			// is specified, respectively.
   234  			destroyStorage = &c.destroyStorage
   235  		}
   236  		err = api.DestroyController(controller.DestroyControllerParams{
   237  			DestroyModels:  c.destroyModels,
   238  			DestroyStorage: destroyStorage,
   239  		})
   240  		if err != nil {
   241  			if params.IsCodeHasHostedModels(err) {
   242  				hasHostedModels = true
   243  			} else if params.IsCodeHasPersistentStorage(err) {
   244  				hasPersistentStorage = true
   245  			} else {
   246  				return c.ensureUserFriendlyErrorLog(
   247  					errors.Annotate(err, "cannot destroy controller"),
   248  					ctx, api,
   249  				)
   250  			}
   251  		}
   252  
   253  		updateStatus := newTimedStatusUpdater(ctx, api, controllerEnviron.Config().UUID(), clock.WallClock)
   254  		// wait for 2 seconds to let empty hosted models changed from alive to dying.
   255  		modelStatus := updateStatus(0)
   256  		if !c.destroyModels {
   257  			if err := c.checkNoAliveHostedModels(ctx, modelStatus.models); err != nil {
   258  				return errors.Trace(err)
   259  			}
   260  			if hasHostedModels && !hasUnDeadModels(modelStatus.models) {
   261  				// When we called DestroyController before, we were
   262  				// informed that there were hosted models remaining.
   263  				// When we checked just now, there were none. We should
   264  				// try destroying again.
   265  				continue
   266  			}
   267  		}
   268  		if !c.destroyStorage && !c.releaseStorage && hasPersistentStorage {
   269  			if err := c.checkNoPersistentStorage(ctx, modelStatus); err != nil {
   270  				return errors.Trace(err)
   271  			}
   272  			// When we called DestroyController before, we were
   273  			// informed that there was persistent storage remaining.
   274  			// When we checked just now, there was none. We should
   275  			// try destroying again.
   276  			continue
   277  		}
   278  
   279  		// Even if we've not just requested for hosted models to be destroyed,
   280  		// there may be some being destroyed already. We need to wait for them.
   281  		// Check for both undead models and live machines, as machines may be
   282  		// in the controller model.
   283  		ctx.Infof("Waiting for hosted model resources to be reclaimed")
   284  		for ; hasUnreclaimedResources(modelStatus); modelStatus = updateStatus(2 * time.Second) {
   285  			ctx.Infof(fmtCtrStatus(modelStatus.controller))
   286  			for _, model := range modelStatus.models {
   287  				ctx.Verbosef(fmtModelStatus(model))
   288  			}
   289  		}
   290  		ctx.Infof("All hosted models reclaimed, cleaning up controller machines")
   291  		return c.environsDestroy(controllerName, controllerEnviron, cloudCallCtx, store)
   292  	}
   293  }
   294  
   295  func (c *destroyCommand) modelHasStorage(modelName string) (bool, error) {
   296  	client, err := c.getStorageAPI(modelName)
   297  	if err != nil {
   298  		return false, errors.Trace(err)
   299  	}
   300  	defer client.Close()
   301  
   302  	storage, err := client.ListStorageDetails()
   303  	if err != nil {
   304  		return false, errors.Trace(err)
   305  	}
   306  	return len(storage) > 0, nil
   307  }
   308  
   309  // checkNoAliveHostedModels ensures that the given set of hosted models
   310  // contains none that are Alive. If there are, an message is printed
   311  // out to
   312  func (c *destroyCommand) checkNoAliveHostedModels(ctx *cmd.Context, models []modelData) error {
   313  	if !hasAliveModels(models) {
   314  		return nil
   315  	}
   316  	// The user did not specify --destroy-all-models,
   317  	// and there are models still alive.
   318  	var buf bytes.Buffer
   319  	for _, model := range models {
   320  		if model.Life != string(params.Alive) {
   321  			continue
   322  		}
   323  		buf.WriteString(fmtModelStatus(model))
   324  		buf.WriteRune('\n')
   325  	}
   326  	controllerName, err := c.ControllerName()
   327  	if err != nil {
   328  		return errors.Trace(err)
   329  	}
   330  	return errors.Errorf(`cannot destroy controller %q
   331  
   332  The controller has live hosted models. If you want
   333  to destroy all hosted models in the controller,
   334  run this command again with the --destroy-all-models
   335  option.
   336  
   337  Models:
   338  %s`, controllerName, buf.String())
   339  }
   340  
   341  // checkNoPersistentStorage ensures that the controller contains
   342  // no persistent storage. If there is any, a message is printed
   343  // out informing the user that they must choose to destroy or
   344  // release the storage.
   345  func (c *destroyCommand) checkNoPersistentStorage(ctx *cmd.Context, envStatus environmentStatus) error {
   346  	models := append([]modelData{envStatus.controller.Model}, envStatus.models...)
   347  
   348  	var modelsWithPersistentStorage int
   349  	var persistentVolumesTotal int
   350  	var persistentFilesystemsTotal int
   351  	for _, m := range models {
   352  		if m.PersistentVolumeCount+m.PersistentFilesystemCount == 0 {
   353  			continue
   354  		}
   355  		modelsWithPersistentStorage++
   356  		persistentVolumesTotal += m.PersistentVolumeCount
   357  		persistentFilesystemsTotal += m.PersistentFilesystemCount
   358  	}
   359  
   360  	var buf bytes.Buffer
   361  	if n := persistentVolumesTotal; n > 0 {
   362  		fmt.Fprintf(&buf, "%d volume", n)
   363  		if n > 1 {
   364  			buf.WriteRune('s')
   365  		}
   366  		if persistentFilesystemsTotal > 0 {
   367  			buf.WriteString(" and ")
   368  		}
   369  	}
   370  	if n := persistentFilesystemsTotal; n > 0 {
   371  		fmt.Fprintf(&buf, "%d filesystem", n)
   372  		if n > 1 {
   373  			buf.WriteRune('s')
   374  		}
   375  	}
   376  	buf.WriteRune(' ')
   377  	if n := modelsWithPersistentStorage; n == 1 {
   378  		buf.WriteString("in 1 model")
   379  	} else {
   380  		fmt.Fprintf(&buf, "across %d models", n)
   381  	}
   382  
   383  	controllerName, err := c.ControllerName()
   384  	if err != nil {
   385  		return errors.Trace(err)
   386  	}
   387  
   388  	return errors.Errorf(`cannot destroy controller %q
   389  
   390  The controller has persistent storage remaining:
   391  	%s
   392  
   393  To destroy the storage, run the destroy-controller
   394  command again with the "--destroy-storage" option.
   395  
   396  To release the storage from Juju's management
   397  without destroying it, use the "--release-storage"
   398  option instead. The storage can then be imported
   399  into another Juju model.
   400  
   401  `, controllerName, buf.String())
   402  }
   403  
   404  // ensureUserFriendlyErrorLog ensures that error will be logged and displayed
   405  // in a user-friendly manner with readable and digestable error message.
   406  func (c *destroyCommand) ensureUserFriendlyErrorLog(destroyErr error, ctx *cmd.Context, api destroyControllerAPI) error {
   407  	if destroyErr == nil {
   408  		return nil
   409  	}
   410  	if params.IsCodeOperationBlocked(destroyErr) {
   411  		logger.Errorf(destroyControllerBlockedMsg)
   412  		if api != nil {
   413  			models, err := api.ListBlockedModels()
   414  			out := &bytes.Buffer{}
   415  			if err == nil {
   416  				var info interface{}
   417  				info, err = block.FormatModelBlockInfo(models)
   418  				if err != nil {
   419  					return errors.Trace(err)
   420  				}
   421  				err = block.FormatTabularBlockedModels(out, info)
   422  			}
   423  			if err != nil {
   424  				logger.Errorf("Unable to list models: %s", err)
   425  				return cmd.ErrSilent
   426  			}
   427  			ctx.Infof(out.String())
   428  		}
   429  		return cmd.ErrSilent
   430  	}
   431  	controllerName, err := c.ControllerName()
   432  	if err != nil {
   433  		return errors.Trace(err)
   434  	}
   435  	logger.Errorf(stdFailureMsg, controllerName)
   436  	return destroyErr
   437  }
   438  
   439  const destroyControllerBlockedMsg = `there are models with disabled commands preventing controller destruction
   440  
   441  To enable controller destruction, please run:
   442  
   443      juju enable-destroy-controller
   444  
   445  `
   446  
   447  // TODO(axw) this should only be printed out if we couldn't
   448  // connect to the controller.
   449  const stdFailureMsg = `failed to destroy controller %q
   450  
   451  If the controller is unusable, then you may run
   452  
   453      juju kill-controller
   454  
   455  to forcibly destroy the controller. Upon doing so, review
   456  your cloud provider console for any resources that need
   457  to be cleaned up.
   458  
   459  `
   460  
   461  // destroyCommandBase provides common attributes and methods that both the controller
   462  // destroy and controller kill commands require.
   463  type destroyCommandBase struct {
   464  	modelcmd.ControllerCommandBase
   465  	assumeYes bool
   466  
   467  	// The following fields are for mocking out
   468  	// api behavior for testing.
   469  	api       destroyControllerAPI
   470  	apierr    error
   471  	clientapi destroyClientAPI
   472  
   473  	controllerCredentialAPIFunc newCredentialAPIFunc
   474  
   475  	environsDestroy func(string, environs.ControllerDestroyer, context.ProviderCallContext, jujuclient.ControllerStore) error
   476  }
   477  
   478  func (c *destroyCommandBase) getControllerAPI() (destroyControllerAPI, error) {
   479  	// Note that some tests set c.api to a non-nil value
   480  	// even when c.apierr is non-nil, hence the separate test.
   481  	if c.apierr != nil {
   482  		return nil, c.apierr
   483  	}
   484  	if c.api != nil {
   485  		return c.api, nil
   486  	}
   487  	root, err := c.NewAPIRoot()
   488  	if err != nil {
   489  		return nil, errors.Trace(err)
   490  	}
   491  	return controller.NewClient(root), nil
   492  }
   493  
   494  func (c *destroyCommand) getStorageAPI(modelName string) (storageAPI, error) {
   495  	if c.storageAPI != nil {
   496  		return c.storageAPI, nil
   497  	}
   498  	root, err := c.NewModelAPIRoot(modelName)
   499  	if err != nil {
   500  		return nil, errors.Trace(err)
   501  	}
   502  	return storage.NewClient(root), nil
   503  }
   504  
   505  // SetFlags implements Command.SetFlags.
   506  func (c *destroyCommandBase) SetFlags(f *gnuflag.FlagSet) {
   507  	c.ControllerCommandBase.SetFlags(f)
   508  	f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation")
   509  	f.BoolVar(&c.assumeYes, "yes", false, "")
   510  }
   511  
   512  // Init implements Command.Init.
   513  func (c *destroyCommandBase) Init(args []string) error {
   514  	switch len(args) {
   515  	case 0:
   516  		return errors.New("no controller specified")
   517  	case 1:
   518  		return c.SetControllerName(args[0], false)
   519  	default:
   520  		return cmd.CheckEmpty(args[1:])
   521  	}
   522  }
   523  
   524  // getControllerEnviron returns the Environ for the controller model.
   525  //
   526  // getControllerEnviron gets the information required to get the
   527  // Environ by first checking the config store, then querying the
   528  // API if the information is not in the store.
   529  func (c *destroyCommandBase) getControllerEnviron(
   530  	ctx *cmd.Context,
   531  	store jujuclient.ClientStore,
   532  	controllerName string,
   533  	sysAPI destroyControllerAPI,
   534  ) (environs.Environ, error) {
   535  	// TODO: (hml) 2018-08-01
   536  	// We should try to destroy via the API first, from store is a
   537  	// fall back position.
   538  	env, err := c.getControllerEnvironFromStore(ctx, store, controllerName)
   539  	if errors.IsNotFound(err) {
   540  		return c.getControllerEnvironFromAPI(sysAPI, controllerName)
   541  	} else if err != nil {
   542  		return nil, errors.Annotate(err, "getting environ using bootstrap config from client store")
   543  	}
   544  	return env, nil
   545  }
   546  
   547  func (c *destroyCommandBase) getControllerEnvironFromStore(
   548  	ctx *cmd.Context,
   549  	store jujuclient.ClientStore,
   550  	controllerName string,
   551  ) (environs.Environ, error) {
   552  	bootstrapConfig, params, err := modelcmd.NewGetBootstrapConfigParamsFunc(
   553  		ctx, store, environs.GlobalProviderRegistry(),
   554  	)(controllerName)
   555  	if err != nil {
   556  		return nil, errors.Trace(err)
   557  	}
   558  	provider, err := environs.Provider(bootstrapConfig.CloudType)
   559  	if err != nil {
   560  		return nil, errors.Trace(err)
   561  	}
   562  	cfg, err := provider.PrepareConfig(*params)
   563  	if err != nil {
   564  		return nil, errors.Trace(err)
   565  	}
   566  	return environs.New(environs.OpenParams{
   567  		Cloud:  params.Cloud,
   568  		Config: cfg,
   569  	})
   570  }
   571  
   572  func (c *destroyCommandBase) getControllerEnvironFromAPI(
   573  	api destroyControllerAPI,
   574  	controllerName string,
   575  ) (environs.Environ, error) {
   576  	if api == nil {
   577  		return nil, errors.New(
   578  			"unable to get bootstrap information from client store or API",
   579  		)
   580  	}
   581  	attrs, err := api.ModelConfig()
   582  	if err != nil {
   583  		return nil, errors.Annotate(err, "getting model config from API")
   584  	}
   585  	cfg, err := config.New(config.NoDefaults, attrs)
   586  	if err != nil {
   587  		return nil, errors.Trace(err)
   588  	}
   589  	cloudSpec, err := api.CloudSpec(names.NewModelTag(cfg.UUID()))
   590  	if err != nil {
   591  		return nil, errors.Annotate(err, "getting cloud spec from API")
   592  	}
   593  	return environs.New(environs.OpenParams{
   594  		Cloud:  cloudSpec,
   595  		Config: cfg,
   596  	})
   597  }
   598  
   599  func confirmDestruction(ctx *cmd.Context, controllerName string) error {
   600  	// Get confirmation from the user that they want to continue
   601  	fmt.Fprintf(ctx.Stdout, destroySysMsg, controllerName)
   602  
   603  	scanner := bufio.NewScanner(ctx.Stdin)
   604  	scanner.Scan()
   605  	err := scanner.Err()
   606  	if err != nil && err != io.EOF {
   607  		return errors.Annotate(err, "controller destruction aborted")
   608  	}
   609  	answer := strings.ToLower(scanner.Text())
   610  	if answer != "y" && answer != "yes" {
   611  		return errors.New("controller destruction aborted")
   612  	}
   613  
   614  	return nil
   615  }
   616  
   617  // CredentialAPI defines the methods on the credential API endpoint that the
   618  // destroy command might call.
   619  type CredentialAPI interface {
   620  	InvalidateModelCredential(string) error
   621  	Close() error
   622  }
   623  
   624  func (c *destroyCommandBase) credentialAPIForControllerModel() (CredentialAPI, error) {
   625  	// Note that the api here needs to operate on a controller model itself,
   626  	// as the controller model's cloud credential is the controller cloud credential.
   627  	root, err := c.NewAPIRoot()
   628  	if err != nil {
   629  		return nil, errors.Trace(err)
   630  	}
   631  	return credentialmanager.NewClient(root), nil
   632  }
   633  
   634  type newCredentialAPIFunc func() (CredentialAPI, error)
   635  
   636  func cloudCallContext(newAPIFunc newCredentialAPIFunc) context.ProviderCallContext {
   637  	callCtx := context.NewCloudCallContext()
   638  	callCtx.InvalidateCredentialFunc = func(reason string) error {
   639  		api, err := newAPIFunc()
   640  		if err != nil {
   641  			return errors.Trace(err)
   642  		}
   643  		defer api.Close()
   644  		return api.InvalidateModelCredential(reason)
   645  	}
   646  	return callCtx
   647  }