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 }