github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/cmd/modelcmd/base.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package modelcmd
     5  
     6  import (
     7  	"bufio"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
    16  	"github.com/juju/cmd/v3"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/gnuflag"
    19  	"github.com/juju/names/v5"
    20  	"golang.org/x/crypto/ssh/terminal"
    21  
    22  	"github.com/juju/juju/api"
    23  	"github.com/juju/juju/api/authentication"
    24  	"github.com/juju/juju/api/base"
    25  	"github.com/juju/juju/api/client/modelmanager"
    26  	k8sproxy "github.com/juju/juju/caas/kubernetes/provider/proxy"
    27  	"github.com/juju/juju/cloud"
    28  	"github.com/juju/juju/core/network"
    29  	"github.com/juju/juju/environs"
    30  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    31  	"github.com/juju/juju/environs/config"
    32  	"github.com/juju/juju/juju"
    33  	"github.com/juju/juju/jujuclient"
    34  	"github.com/juju/juju/pki"
    35  	proxyerrors "github.com/juju/juju/proxy/errors"
    36  	"github.com/juju/juju/rpc/params"
    37  )
    38  
    39  var errNoNameSpecified = errors.New("no name specified")
    40  
    41  type modelMigratedError string
    42  
    43  func newModelMigratedError(store jujuclient.ClientStore, modelName string, redirErr *api.RedirectError) error {
    44  	// Check if this is a known controller
    45  	allEndpoints := network.CollapseToHostPorts(redirErr.Servers).Strings()
    46  	_, existingName, err := store.ControllerByAPIEndpoints(allEndpoints...)
    47  	if err != nil && !errors.IsNotFound(err) {
    48  		return err
    49  	}
    50  
    51  	if existingName != "" {
    52  		mErr := fmt.Sprintf(`Model %q has been migrated to controller %q.
    53  To access it run 'juju switch %s:%s'.`, modelName, existingName, existingName, modelName)
    54  
    55  		return modelMigratedError(mErr)
    56  	}
    57  
    58  	// CACerts are always valid so no error checking is required here.
    59  	fingerprint, _, err := pki.Fingerprint([]byte(redirErr.CACert))
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	ctrlAlias := "new-controller"
    65  	if redirErr.ControllerAlias != "" {
    66  		ctrlAlias = redirErr.ControllerAlias
    67  	}
    68  
    69  	var loginCmds []string
    70  	for _, endpoint := range allEndpoints {
    71  		loginCmds = append(loginCmds, fmt.Sprintf("  'juju login %s -c %s'", endpoint, ctrlAlias))
    72  	}
    73  
    74  	mErr := fmt.Sprintf(`Model %q has been migrated to another controller.
    75  To access it run one of the following commands (you can replace the -c argument with your own preferred controller name):
    76  %s
    77  
    78  New controller fingerprint [%s]`, modelName, strings.Join(loginCmds, "\n"), fingerprint)
    79  
    80  	return modelMigratedError(mErr)
    81  }
    82  
    83  func (e modelMigratedError) Error() string {
    84  	return string(e)
    85  }
    86  
    87  // IsModelMigratedError returns true if err is of type modelMigratedError.
    88  func IsModelMigratedError(err error) bool {
    89  	_, ok := errors.Cause(err).(modelMigratedError)
    90  	return ok
    91  }
    92  
    93  // Command extends cmd.Command with a closeContext method.
    94  // It is implicitly implemented by any type that embeds CommandBase.
    95  type Command interface {
    96  	cmd.Command
    97  
    98  	// SetAPIOpen sets the function used for opening an API connection.
    99  	SetAPIOpen(opener api.OpenFunc)
   100  
   101  	// SetModelAPI sets the api used to access model information.
   102  	SetModelAPI(api ModelAPI)
   103  
   104  	// SetEmbedded sets whether the command is being run inside a controller.
   105  	SetEmbedded(bool)
   106  
   107  	// closeAPIContexts closes any API contexts that have been opened.
   108  	closeAPIContexts()
   109  	initContexts(*cmd.Context)
   110  	setRunStarted()
   111  }
   112  
   113  // ModelAPI provides access to the model client facade methods.
   114  type ModelAPI interface {
   115  	ListModels(user string) ([]base.UserModel, error)
   116  	Close() error
   117  }
   118  
   119  // CommandBase is a convenience type for embedding that need
   120  // an API connection.
   121  type CommandBase struct {
   122  	cmd.CommandBase
   123  	FilesystemCommand
   124  	cmdContext    *cmd.Context
   125  	apiContexts   map[string]*apiContext
   126  	modelAPI_     ModelAPI
   127  	apiOpenFunc   api.OpenFunc
   128  	authOpts      AuthOpts
   129  	runStarted    bool
   130  	refreshModels func(jujuclient.ClientStore, string) error
   131  
   132  	// StdContext is the Go context.
   133  	StdContext context.Context
   134  
   135  	// CanClearCurrentModel indicates that this command can reset current model in local cache, aka client store.
   136  	CanClearCurrentModel bool
   137  
   138  	// Embedded is true if this command is being run inside a controller.
   139  	Embedded bool
   140  }
   141  
   142  func (c *CommandBase) assertRunStarted() {
   143  	if !c.runStarted {
   144  		panic("inappropriate method called at init time")
   145  	}
   146  }
   147  
   148  func (c *CommandBase) setRunStarted() {
   149  	c.runStarted = true
   150  }
   151  
   152  // closeAPIContexts closes any API contexts that have
   153  // been created.
   154  //
   155  //nolint:unused
   156  func (c *CommandBase) closeAPIContexts() {
   157  	for name, ctx := range c.apiContexts {
   158  		if err := ctx.Close(); err != nil {
   159  			logger.Errorf("%v", err)
   160  		}
   161  		delete(c.apiContexts, name)
   162  	}
   163  }
   164  
   165  // SetEmbedded sets whether the command is embedded.
   166  func (c *CommandBase) SetEmbedded(embedded bool) {
   167  	c.Embedded = embedded
   168  	if embedded {
   169  		c.filesystem = restrictedFilesystem{}
   170  	} else {
   171  		c.filesystem = osFilesystem{}
   172  	}
   173  }
   174  
   175  // SetFlags implements cmd.Command.SetFlags.
   176  func (c *CommandBase) SetFlags(f *gnuflag.FlagSet) {
   177  	c.authOpts.SetFlags(f)
   178  }
   179  
   180  // SetModelAPI sets the api used to access model information.
   181  func (c *CommandBase) SetModelAPI(api ModelAPI) {
   182  	c.modelAPI_ = api
   183  }
   184  
   185  // SetAPIOpen sets the function used for opening an API connection.
   186  func (c *CommandBase) SetAPIOpen(apiOpen api.OpenFunc) {
   187  	c.apiOpenFunc = apiOpen
   188  }
   189  
   190  // SetModelRefresh sets the function used for refreshing models.
   191  func (c *CommandBase) SetModelRefresh(refresh func(jujuclient.ClientStore, string) error) {
   192  	c.refreshModels = refresh
   193  }
   194  
   195  func (c *CommandBase) modelAPI(store jujuclient.ClientStore, controllerName string) (ModelAPI, error) {
   196  	c.assertRunStarted()
   197  	if c.modelAPI_ != nil {
   198  		return c.modelAPI_, nil
   199  	}
   200  	conn, err := c.NewAPIRoot(store, controllerName, "")
   201  	if err != nil {
   202  		return nil, errors.Trace(err)
   203  	}
   204  	c.modelAPI_ = modelmanager.NewClient(conn)
   205  	return c.modelAPI_, nil
   206  }
   207  
   208  // NewAPIRoot returns a new connection to the API server for the given
   209  // model or controller.
   210  func (c *CommandBase) NewAPIRoot(
   211  	store jujuclient.ClientStore,
   212  	controllerName, modelName string,
   213  ) (api.Connection, error) {
   214  	return c.NewAPIRootWithDialOpts(store, controllerName, modelName, nil)
   215  }
   216  
   217  func processAccountDetails(accountDetails *jujuclient.AccountDetails) *jujuclient.AccountDetails {
   218  	if accountDetails != nil && accountDetails.Type != "" && accountDetails.Type != jujuclient.UserPassAccountDetailsType {
   219  		return accountDetails
   220  	}
   221  	// If there are no account details or there's no logged-in
   222  	// user or the user is external, then trigger macaroon authentication
   223  	// by using an empty AccountDetails.
   224  	if accountDetails == nil || accountDetails.User == "" {
   225  		accountDetails = &jujuclient.AccountDetails{}
   226  	} else {
   227  		u := names.NewUserTag(accountDetails.User)
   228  		if !u.IsLocal() {
   229  			if len(accountDetails.Macaroons) == 0 {
   230  				accountDetails = &jujuclient.AccountDetails{}
   231  			} else {
   232  				// If the account has macaroon set, use those to login
   233  				// to avoid an unnecessary auth round trip.
   234  				// Used for embedded commands.
   235  				accountDetails = &jujuclient.AccountDetails{
   236  					User:      u.Id(),
   237  					Macaroons: accountDetails.Macaroons,
   238  				}
   239  			}
   240  		}
   241  	}
   242  	return accountDetails
   243  }
   244  
   245  // NewAPIRootWithDialOpts returns a new connection to the API server for the
   246  // given model or controller (the default dial options will be overridden if
   247  // dialOpts is not nil).
   248  func (c *CommandBase) NewAPIRootWithDialOpts(
   249  	store jujuclient.ClientStore,
   250  	controllerName, modelName string,
   251  	dialOpts *api.DialOpts,
   252  ) (api.Connection, error) {
   253  	c.assertRunStarted()
   254  	accountDetails, err := store.AccountDetails(controllerName)
   255  	if err != nil && !errors.Is(err, errors.NotFound) {
   256  		return nil, errors.Trace(err)
   257  	}
   258  
   259  	accountDetails = processAccountDetails(accountDetails)
   260  
   261  	param, err := c.NewAPIConnectionParams(
   262  		store, controllerName, modelName, accountDetails,
   263  	)
   264  	if err != nil {
   265  		return nil, errors.Trace(err)
   266  	}
   267  	if dialOpts != nil {
   268  		param.DialOpts = *dialOpts
   269  	}
   270  	conn, err := juju.NewAPIConnection(param)
   271  	if modelName != "" && params.ErrCode(err) == params.CodeModelNotFound {
   272  		return nil, c.missingModelError(store, controllerName, modelName)
   273  	}
   274  	if redirErr, ok := errors.Cause(err).(*api.RedirectError); ok {
   275  		return nil, newModelMigratedError(store, modelName, redirErr)
   276  	}
   277  	if juju.IsNoAddressesError(err) {
   278  		return nil, errors.New("no controller API addresses; is bootstrap still in progress?")
   279  	}
   280  	if proxyerrors.IsProxyConnectError(err) {
   281  		logger.Debugf("proxy connection error: %v", err)
   282  		if proxyerrors.ProxyType(err) == k8sproxy.ProxierTypeKey {
   283  			return nil, errors.Annotate(err, "cannot connect to k8s api server; try running 'juju update-k8s --client <k8s cloud name>'")
   284  		}
   285  		return nil, errors.Annotate(err, "cannot connect to api server proxy")
   286  	}
   287  	return conn, errors.Trace(err)
   288  }
   289  
   290  // RemoveModelFromClientStore removes given model from client cache, store,
   291  // for a given controller.
   292  // If this model has also been cached as current, it will be reset if
   293  // the requesting command can modify current model.
   294  // For example, commands such as add/destroy-model, login/register, etc.
   295  // If the model was cached as current but the command is not expected to
   296  // change current model, this call will still remove model details from the client cache
   297  // but will keep current model name intact to allow subsequent calls to try to resolve
   298  // model details on the controller.
   299  func (c *CommandBase) RemoveModelFromClientStore(store jujuclient.ClientStore, controllerName, modelName string) {
   300  	err := store.RemoveModel(controllerName, modelName)
   301  	if err != nil && !errors.IsNotFound(err) {
   302  		logger.Warningf("cannot remove unknown model from cache: %v", err)
   303  	}
   304  	if c.CanClearCurrentModel {
   305  		currentModel, err := store.CurrentModel(controllerName)
   306  		if err != nil {
   307  			logger.Warningf("cannot read current model: %v", err)
   308  		} else if currentModel == modelName {
   309  			if err := store.SetCurrentModel(controllerName, ""); err != nil {
   310  				logger.Warningf("cannot reset current model: %v", err)
   311  			}
   312  		}
   313  	}
   314  }
   315  
   316  func (c *CommandBase) missingModelError(store jujuclient.ClientStore, controllerName, modelName string) error {
   317  	// First, we'll try and clean up the missing model from the local cache.
   318  	c.RemoveModelFromClientStore(store, controllerName, modelName)
   319  	return errors.Errorf("model %q has been removed from the controller, run 'juju models' and switch to one of them.", modelName)
   320  }
   321  
   322  // NewAPIConnectionParams returns a juju.NewAPIConnectionParams with the
   323  // given arguments such that a call to juju.NewAPIConnection with the
   324  // result behaves the same as a call to CommandBase.NewAPIRoot with
   325  // the same arguments.
   326  func (c *CommandBase) NewAPIConnectionParams(
   327  	store jujuclient.ClientStore,
   328  	controllerName, modelName string,
   329  	accountDetails *jujuclient.AccountDetails,
   330  ) (juju.NewAPIConnectionParams, error) {
   331  	c.assertRunStarted()
   332  	bakeryClient, err := c.BakeryClient(store, controllerName)
   333  	if err != nil {
   334  		return juju.NewAPIConnectionParams{}, errors.Trace(err)
   335  	}
   336  	var getPassword func(username string) (string, error)
   337  	var printOutput func(format string, a ...any) error
   338  	if c.cmdContext != nil {
   339  		getPassword = func(username string) (string, error) {
   340  			fmt.Fprintf(c.cmdContext.Stderr, "please enter password for %s on %s: ", username, controllerName)
   341  			defer fmt.Fprintln(c.cmdContext.Stderr)
   342  			return readPassword(c.cmdContext.Stdin)
   343  		}
   344  		printOutput = func(format string, a ...any) error {
   345  			_, err := fmt.Fprintf(c.cmdContext.Stderr, format, a...)
   346  			return err
   347  		}
   348  	} else {
   349  		getPassword = func(username string) (string, error) {
   350  			return "", errors.New("no context to prompt for password")
   351  		}
   352  		printOutput = func(_ string, _ ...any) error {
   353  			return errors.New("no context to print output")
   354  		}
   355  	}
   356  
   357  	return newAPIConnectionParams(
   358  		store, controllerName, modelName,
   359  		accountDetails,
   360  		c.Embedded,
   361  		bakeryClient,
   362  		c.apiOpen,
   363  		getPassword,
   364  		printOutput,
   365  	)
   366  }
   367  
   368  // HTTPClient returns an http.Client that contains the loaded
   369  // persistent cookie jar. Note that this client is not good for
   370  // connecting to the Juju API itself because it does not
   371  // have the correct TLS setup - use api.Connection.HTTPClient
   372  // for that.
   373  func (c *CommandBase) HTTPClient(store jujuclient.ClientStore, controllerName string) (*http.Client, error) {
   374  	c.assertRunStarted()
   375  	bakeryClient, err := c.BakeryClient(store, controllerName)
   376  	if err != nil {
   377  		return nil, errors.Trace(err)
   378  	}
   379  	return bakeryClient.Client, nil
   380  }
   381  
   382  // BakeryClient returns a macaroon bakery client that
   383  // uses the same HTTP client returned by HTTPClient.
   384  func (c *CommandBase) BakeryClient(store jujuclient.CookieStore, controllerName string) (*httpbakery.Client, error) {
   385  	c.assertRunStarted()
   386  	ctx, err := c.getAPIContext(store, controllerName)
   387  	if err != nil {
   388  		return nil, errors.Trace(err)
   389  	}
   390  	return ctx.NewBakeryClient(), nil
   391  }
   392  
   393  // APIOpen establishes a connection to the API server using the
   394  // the given api.Info and api.DialOpts, and associating any stored
   395  // authorization tokens with the given controller name.
   396  func (c *CommandBase) APIOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) {
   397  	c.assertRunStarted()
   398  	return c.apiOpen(info, opts)
   399  }
   400  
   401  // apiOpen establishes a connection to the API server using the
   402  // the give api.Info and api.DialOpts.
   403  func (c *CommandBase) apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) {
   404  	if c.apiOpenFunc != nil {
   405  		return c.apiOpenFunc(info, opts)
   406  	}
   407  	return api.Open(info, opts)
   408  }
   409  
   410  // RefreshModels refreshes the local models cache for the current user
   411  // on the specified controller.
   412  func (c *CommandBase) RefreshModels(store jujuclient.ClientStore, controllerName string) error {
   413  	if c.refreshModels == nil {
   414  		return c.doRefreshModels(store, controllerName)
   415  	}
   416  	return c.refreshModels(store, controllerName)
   417  }
   418  
   419  func (c *CommandBase) doRefreshModels(store jujuclient.ClientStore, controllerName string) error {
   420  	c.assertRunStarted()
   421  	modelManager, err := c.modelAPI(store, controllerName)
   422  	if err != nil {
   423  		return errors.Trace(err)
   424  	}
   425  	defer func() { _ = modelManager.Close() }()
   426  
   427  	accountDetails, err := store.AccountDetails(controllerName)
   428  	if err != nil {
   429  		return errors.Trace(err)
   430  	}
   431  
   432  	models, err := modelManager.ListModels(accountDetails.User)
   433  	if err != nil {
   434  		return errors.Trace(err)
   435  	}
   436  	if err := c.SetControllerModels(store, controllerName, models); err != nil {
   437  		return errors.Trace(err)
   438  	}
   439  	return nil
   440  }
   441  
   442  func (c *CommandBase) SetControllerModels(store jujuclient.ClientStore, controllerName string, models []base.UserModel) error {
   443  	modelsToStore := make(map[string]jujuclient.ModelDetails, len(models))
   444  	for _, model := range models {
   445  		modelDetails := jujuclient.ModelDetails{ModelUUID: model.UUID, ModelType: model.Type}
   446  		owner := names.NewUserTag(model.Owner)
   447  		modelName := jujuclient.JoinOwnerModelName(owner, model.Name)
   448  		modelsToStore[modelName] = modelDetails
   449  	}
   450  	if err := store.SetModels(controllerName, modelsToStore); err != nil {
   451  		return errors.Trace(err)
   452  	}
   453  	return nil
   454  }
   455  
   456  // ModelUUIDs returns the model UUIDs for the given model names.
   457  func (c *CommandBase) ModelUUIDs(store jujuclient.ClientStore, controllerName string, modelNames []string) ([]string, error) {
   458  	var result []string
   459  	for _, modelName := range modelNames {
   460  		model, err := store.ModelByName(controllerName, modelName)
   461  		if errors.IsNotFound(err) {
   462  			// The model isn't known locally, so query the models available in the controller.
   463  			logger.Infof("model %q not cached locally, refreshing models from controller", modelName)
   464  			if err := c.RefreshModels(store, controllerName); err != nil {
   465  				return nil, errors.Annotatef(err, "refreshing model %q", modelName)
   466  			}
   467  			model, err = store.ModelByName(controllerName, modelName)
   468  		}
   469  		if err != nil {
   470  			return nil, errors.Trace(err)
   471  		}
   472  		result = append(result, model.ModelUUID)
   473  	}
   474  	return result, nil
   475  }
   476  
   477  // ControllerUUID returns the controller UUID for specified controller name.
   478  func (c *CommandBase) ControllerUUID(store jujuclient.ClientStore, controllerName string) (string, error) {
   479  	ctrl, err := store.ControllerByName(controllerName)
   480  	if err != nil {
   481  		return "", errors.Annotate(err, "resolving controller name")
   482  	}
   483  	return ctrl.ControllerUUID, nil
   484  }
   485  
   486  // getAPIContext returns an apiContext for the given controller.
   487  // It will return the same context if called twice for the same controller.
   488  // The context will be closed when closeAPIContexts is called.
   489  func (c *CommandBase) getAPIContext(store jujuclient.CookieStore, controllerName string) (*apiContext, error) {
   490  	c.assertRunStarted()
   491  	if ctx := c.apiContexts[controllerName]; ctx != nil {
   492  		return ctx, nil
   493  	}
   494  	if controllerName == "" {
   495  		return nil, errors.New("cannot get API context from empty controller name")
   496  	}
   497  	c.authOpts.Embedded = c.Embedded
   498  	ctx, err := newAPIContext(c.cmdContext, &c.authOpts, store, controllerName)
   499  	if err != nil {
   500  		return nil, errors.Trace(err)
   501  	}
   502  	c.apiContexts[controllerName] = ctx
   503  	return ctx, nil
   504  }
   505  
   506  // CookieJar returns the cookie jar that is used to store auth credentials
   507  // when connecting to the API.
   508  func (c *CommandBase) CookieJar(store jujuclient.CookieStore, controllerName string) (http.CookieJar, error) {
   509  	ctx, err := c.getAPIContext(store, controllerName)
   510  	if err != nil {
   511  		return nil, errors.Trace(err)
   512  	}
   513  	return ctx.CookieJar(), nil
   514  }
   515  
   516  // ClearControllerMacaroons will remove all macaroons stored
   517  // for the given controller from the persistent cookie jar.
   518  // This is called both from 'juju logout' and a failed 'juju register'.
   519  func (c *CommandBase) ClearControllerMacaroons(store jujuclient.CookieStore, controllerName string) error {
   520  	ctx, err := c.getAPIContext(store, controllerName)
   521  	if err != nil {
   522  		return errors.Trace(err)
   523  	}
   524  	ctx.jar.RemoveAll()
   525  	return nil
   526  }
   527  
   528  func (c *CommandBase) initContexts(ctx *cmd.Context) {
   529  	c.StdContext = context.Background()
   530  	c.cmdContext = ctx
   531  	c.apiContexts = make(map[string]*apiContext)
   532  }
   533  
   534  // WrapBase wraps the specified Command. This should be
   535  // used by any command that embeds CommandBase.
   536  func WrapBase(c Command) Command {
   537  	return &baseCommandWrapper{
   538  		Command: c,
   539  	}
   540  }
   541  
   542  type baseCommandWrapper struct {
   543  	Command
   544  }
   545  
   546  // inner implements wrapper.inner.
   547  func (w *baseCommandWrapper) inner() cmd.Command {
   548  	return w.Command
   549  }
   550  
   551  type hasClientStore interface {
   552  	SetClientStore(store jujuclient.ClientStore)
   553  }
   554  
   555  // SetClientStore sets the client store to use.
   556  func (w *baseCommandWrapper) SetClientStore(store jujuclient.ClientStore) {
   557  	if csc, ok := w.Command.(hasClientStore); ok {
   558  		csc.SetClientStore(store)
   559  	}
   560  }
   561  
   562  // SetEmbedded implements the ModelCommand interface.
   563  func (c *baseCommandWrapper) SetEmbedded(embedded bool) {
   564  	c.Command.SetEmbedded(embedded)
   565  }
   566  
   567  // Run implements Command.Run.
   568  func (w *baseCommandWrapper) Run(ctx *cmd.Context) error {
   569  	defer w.closeAPIContexts()
   570  	w.initContexts(ctx)
   571  	w.setRunStarted()
   572  	return w.Command.Run(ctx)
   573  }
   574  
   575  func newAPIConnectionParams(
   576  	store jujuclient.ClientStore,
   577  	controllerName,
   578  	modelName string,
   579  	accountDetails *jujuclient.AccountDetails,
   580  	embedded bool,
   581  	bakery *httpbakery.Client,
   582  	apiOpen api.OpenFunc,
   583  	getPassword func(string) (string, error),
   584  	printOutput func(string, ...any) error,
   585  ) (juju.NewAPIConnectionParams, error) {
   586  	if controllerName == "" {
   587  		return juju.NewAPIConnectionParams{}, errors.Trace(errNoNameSpecified)
   588  	}
   589  	var modelUUID string
   590  	if modelName != "" {
   591  		modelDetails, err := store.ModelByName(controllerName, modelName)
   592  		if err != nil {
   593  			return juju.NewAPIConnectionParams{}, errors.Trace(err)
   594  		}
   595  		modelUUID = modelDetails.ModelUUID
   596  	}
   597  	dialOpts := api.DefaultDialOpts()
   598  	dialOpts.BakeryClient = bakery
   599  
   600  	if accountDetails.Type == jujuclient.OAuth2DeviceFlowAccountDetailsType {
   601  		dialOpts.LoginProvider = api.NewSessionTokenLoginProvider(
   602  			accountDetails.SessionToken,
   603  			printOutput,
   604  			func(sessionToken string) error {
   605  				accountDetails.Type = jujuclient.OAuth2DeviceFlowAccountDetailsType
   606  				accountDetails.SessionToken = sessionToken
   607  				return store.UpdateAccount(controllerName, *accountDetails)
   608  			},
   609  		)
   610  	}
   611  
   612  	// Embedded clients with macaroons cannot discharge.
   613  	if accountDetails != nil && !embedded {
   614  		bakery.InteractionMethods = []httpbakery.Interactor{
   615  			authentication.NewInteractor(accountDetails.User, getPassword),
   616  			httpbakery.WebBrowserInteractor{},
   617  		}
   618  	}
   619  
   620  	return juju.NewAPIConnectionParams{
   621  		Store:          store,
   622  		ControllerName: controllerName,
   623  		AccountDetails: accountDetails,
   624  		ModelUUID:      modelUUID,
   625  		DialOpts:       dialOpts,
   626  		OpenAPI:        OpenAPIFuncWithMacaroons(apiOpen, store, controllerName),
   627  	}, nil
   628  }
   629  
   630  // OpenAPIFuncWithMacaroons is a middleware to ensure that we have a set of
   631  // macaroons for a given open request.
   632  func OpenAPIFuncWithMacaroons(apiOpen api.OpenFunc, store jujuclient.ClientStore, controllerName string) api.OpenFunc {
   633  	return func(info *api.Info, dialOpts api.DialOpts) (api.Connection, error) {
   634  		// When attempting to connect to the non websocket fronted HTTPS
   635  		// endpoints, we need to ensure that we have a series of macaroons
   636  		// correctly set if there isn't a password.
   637  		if info != nil && info.Password == "" && len(info.Macaroons) == 0 {
   638  			cookieJar, err := store.CookieJar(controllerName)
   639  			if err != nil {
   640  				return nil, errors.Trace(err)
   641  			}
   642  
   643  			cookieURL := api.CookieURLFromHost(api.PerferredHost(info))
   644  			info.Macaroons = httpbakery.MacaroonsForURL(cookieJar, cookieURL)
   645  		}
   646  
   647  		return apiOpen(info, dialOpts)
   648  	}
   649  }
   650  
   651  // NewGetBootstrapConfigParamsFunc returns a function that, given a controller name,
   652  // returns the params needed to bootstrap a fresh copy of that controller in the given client store.
   653  func NewGetBootstrapConfigParamsFunc(
   654  	ctx *cmd.Context,
   655  	store jujuclient.ClientStore,
   656  	providerRegistry environs.ProviderRegistry,
   657  ) func(string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) {
   658  	return bootstrapConfigGetter{ctx, store, providerRegistry}.getBootstrapConfigParams
   659  }
   660  
   661  type bootstrapConfigGetter struct {
   662  	ctx      *cmd.Context
   663  	store    jujuclient.ClientStore
   664  	registry environs.ProviderRegistry
   665  }
   666  
   667  func (g bootstrapConfigGetter) getBootstrapConfigParams(controllerName string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) {
   668  	controllerDetails, err := g.store.ControllerByName(controllerName)
   669  	if err != nil {
   670  		return nil, nil, errors.Annotate(err, "resolving controller name")
   671  	}
   672  	bootstrapConfig, err := g.store.BootstrapConfigForController(controllerName)
   673  	if err != nil {
   674  		return nil, nil, errors.Annotate(err, "getting bootstrap config")
   675  	}
   676  
   677  	var credential *cloud.Credential
   678  	bootstrapCloud := cloud.Cloud{
   679  		Name:             bootstrapConfig.Cloud,
   680  		Type:             bootstrapConfig.CloudType,
   681  		Endpoint:         bootstrapConfig.CloudEndpoint,
   682  		IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint,
   683  	}
   684  	if bootstrapConfig.Credential != "" {
   685  		if bootstrapConfig.CloudRegion != "" {
   686  			bootstrapCloud.Regions = []cloud.Region{{
   687  				Name:             bootstrapConfig.CloudRegion,
   688  				Endpoint:         bootstrapConfig.CloudEndpoint,
   689  				IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint,
   690  			}}
   691  		}
   692  		credential, _, _, err = GetCredentials(
   693  			g.ctx, g.store,
   694  			GetCredentialsParams{
   695  				Cloud:          bootstrapCloud,
   696  				CloudRegion:    bootstrapConfig.CloudRegion,
   697  				CredentialName: bootstrapConfig.Credential,
   698  			},
   699  		)
   700  		if err != nil {
   701  			return nil, nil, errors.Trace(err)
   702  		}
   703  	} else {
   704  		// The credential was auto-detected; run auto-detection again.
   705  		provider, err := g.registry.Provider(bootstrapConfig.CloudType)
   706  		if err != nil {
   707  			return nil, nil, errors.Trace(err)
   708  		}
   709  		cloudCredential, err := DetectCredential(bootstrapConfig.Cloud, provider)
   710  		if err != nil {
   711  			return nil, nil, errors.Trace(err)
   712  		}
   713  		// DetectCredential ensures that there is only one credential
   714  		// to choose from. It's still in a map, though, hence for..range.
   715  		var credentialName string
   716  		for name, v := range cloudCredential.AuthCredentials {
   717  			one := v
   718  			credential = &one
   719  			credentialName = name
   720  			break
   721  		}
   722  		credential, err = FinalizeFileContent(credential, provider)
   723  		if err != nil {
   724  			return nil, nil, AnnotateWithFinalizationError(err, credentialName, bootstrapCloud.Name)
   725  		}
   726  		credential, err = provider.FinalizeCredential(
   727  			g.ctx, environs.FinalizeCredentialParams{
   728  				Credential:            *credential,
   729  				CloudName:             bootstrapConfig.Cloud,
   730  				CloudEndpoint:         bootstrapConfig.CloudEndpoint,
   731  				CloudStorageEndpoint:  bootstrapConfig.CloudStorageEndpoint,
   732  				CloudIdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint,
   733  			},
   734  		)
   735  		if err != nil {
   736  			return nil, nil, errors.Trace(err)
   737  		}
   738  	}
   739  
   740  	// Add attributes from the controller details.
   741  	bootstrapConfig.Config[config.UUIDKey] = bootstrapConfig.ControllerModelUUID
   742  	cfg, err := config.New(config.NoDefaults, bootstrapConfig.Config)
   743  	if err != nil {
   744  		return nil, nil, errors.Trace(err)
   745  	}
   746  	return bootstrapConfig, &environs.PrepareConfigParams{
   747  		Cloud: environscloudspec.CloudSpec{
   748  			Type:              bootstrapConfig.CloudType,
   749  			Name:              bootstrapConfig.Cloud,
   750  			Region:            bootstrapConfig.CloudRegion,
   751  			Endpoint:          bootstrapConfig.CloudEndpoint,
   752  			IdentityEndpoint:  bootstrapConfig.CloudIdentityEndpoint,
   753  			StorageEndpoint:   bootstrapConfig.CloudStorageEndpoint,
   754  			Credential:        credential,
   755  			CACertificates:    bootstrapConfig.CloudCACertificates,
   756  			SkipTLSVerify:     bootstrapConfig.SkipTLSVerify,
   757  			IsControllerCloud: bootstrapConfig.Cloud == controllerDetails.Cloud,
   758  		},
   759  		Config: cfg,
   760  	}, nil
   761  }
   762  
   763  // TODO(axw) this is now in three places: change-password,
   764  // register, and here. Refactor and move to a common location.
   765  func readPassword(stdin io.Reader) (string, error) {
   766  	if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
   767  		password, err := terminal.ReadPassword(int(f.Fd()))
   768  		return string(password), err
   769  	}
   770  	return readLine(stdin)
   771  }
   772  
   773  func readLine(stdin io.Reader) (string, error) {
   774  	// Read one byte at a time to avoid reading beyond the delimiter.
   775  	line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n')
   776  	if err != nil {
   777  		return "", errors.Trace(err)
   778  	}
   779  	return line[:len(line)-1], nil
   780  }
   781  
   782  type byteAtATimeReader struct {
   783  	io.Reader
   784  }
   785  
   786  // Read is part of the io.Reader interface.
   787  func (r byteAtATimeReader) Read(out []byte) (int, error) {
   788  	return r.Reader.Read(out[:1])
   789  }
   790  
   791  // wrapper is implemented by types that wrap a command.
   792  type wrapper interface {
   793  	inner() cmd.Command
   794  }
   795  
   796  // InnerCommand returns the command that has been wrapped
   797  // by one of the Wrap functions. This is useful for
   798  // tests that wish to inspect internal details of a command
   799  // instance. If c isn't wrapping anything, it returns c.
   800  func InnerCommand(c cmd.Command) cmd.Command {
   801  	for {
   802  		c1, ok := c.(wrapper)
   803  		if !ok {
   804  			return c
   805  		}
   806  		c = c1.inner()
   807  	}
   808  }