github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "fmt" 9 "io" 10 "net/http" 11 "os" 12 13 "github.com/juju/cmd" 14 "github.com/juju/errors" 15 "github.com/juju/gnuflag" 16 "golang.org/x/crypto/ssh/terminal" 17 "gopkg.in/juju/names.v2" 18 "gopkg.in/macaroon-bakery.v2-unstable/httpbakery" 19 20 "github.com/juju/juju/api" 21 "github.com/juju/juju/api/authentication" 22 "github.com/juju/juju/api/base" 23 "github.com/juju/juju/api/modelmanager" 24 "github.com/juju/juju/apiserver/params" 25 "github.com/juju/juju/cloud" 26 "github.com/juju/juju/environs" 27 "github.com/juju/juju/environs/config" 28 "github.com/juju/juju/juju" 29 "github.com/juju/juju/jujuclient" 30 ) 31 32 var errNoNameSpecified = errors.New("no name specified") 33 34 // Command extends cmd.Command with a closeContext method. 35 // It is implicitly implemented by any type that embeds CommandBase. 36 type Command interface { 37 cmd.Command 38 39 // SetAPIOpen sets the function used for opening an API connection. 40 SetAPIOpen(opener api.OpenFunc) 41 42 // SetModelAPI sets the api used to access model information. 43 SetModelAPI(api ModelAPI) 44 45 // closeAPIContexts closes any API contexts that have been opened. 46 closeAPIContexts() 47 initContexts(*cmd.Context) 48 setRunStarted() 49 } 50 51 // ModelAPI provides access to the model client facade methods. 52 type ModelAPI interface { 53 ListModels(user string) ([]base.UserModel, error) 54 Close() error 55 } 56 57 // CommandBase is a convenience type for embedding that need 58 // an API connection. 59 type CommandBase struct { 60 cmd.CommandBase 61 cmdContext *cmd.Context 62 apiContexts map[string]*apiContext 63 modelAPI_ ModelAPI 64 apiOpenFunc api.OpenFunc 65 authOpts AuthOpts 66 runStarted bool 67 refreshModels func(jujuclient.ClientStore, string) error 68 69 // CanClearCurrentModel indicates that this command can reset current model in local cache, aka client store. 70 CanClearCurrentModel bool 71 } 72 73 func (c *CommandBase) assertRunStarted() { 74 if !c.runStarted { 75 panic("inappropriate method called at init time") 76 } 77 } 78 79 func (c *CommandBase) setRunStarted() { 80 c.runStarted = true 81 } 82 83 // closeAPIContexts closes any API contexts that have 84 // been created. 85 func (c *CommandBase) closeAPIContexts() { 86 for name, ctx := range c.apiContexts { 87 if err := ctx.Close(); err != nil { 88 logger.Errorf("%v", err) 89 } 90 delete(c.apiContexts, name) 91 } 92 } 93 94 // SetFlags implements cmd.Command.SetFlags. 95 func (c *CommandBase) SetFlags(f *gnuflag.FlagSet) { 96 c.authOpts.SetFlags(f) 97 } 98 99 // SetModelAPI sets the api used to access model information. 100 func (c *CommandBase) SetModelAPI(api ModelAPI) { 101 c.modelAPI_ = api 102 } 103 104 // SetAPIOpen sets the function used for opening an API connection. 105 func (c *CommandBase) SetAPIOpen(apiOpen api.OpenFunc) { 106 c.apiOpenFunc = apiOpen 107 } 108 109 // SetModelRefresh sets the function used for refreshing models. 110 func (c *CommandBase) SetModelRefresh(refresh func(jujuclient.ClientStore, string) error) { 111 c.refreshModels = refresh 112 } 113 114 func (c *CommandBase) modelAPI(store jujuclient.ClientStore, controllerName string) (ModelAPI, error) { 115 c.assertRunStarted() 116 if c.modelAPI_ != nil { 117 return c.modelAPI_, nil 118 } 119 conn, err := c.NewAPIRoot(store, controllerName, "") 120 if err != nil { 121 return nil, errors.Trace(err) 122 } 123 c.modelAPI_ = modelmanager.NewClient(conn) 124 return c.modelAPI_, nil 125 } 126 127 // NewAPIRoot returns a new connection to the API server for the given 128 // model or controller. 129 func (c *CommandBase) NewAPIRoot( 130 store jujuclient.ClientStore, 131 controllerName, modelName string, 132 ) (api.Connection, error) { 133 c.assertRunStarted() 134 accountDetails, err := store.AccountDetails(controllerName) 135 if err != nil && !errors.IsNotFound(err) { 136 return nil, errors.Trace(err) 137 } 138 // If there are no account details or there's no logged-in 139 // user or the user is external, then trigger macaroon authentication 140 // by using an empty AccountDetails. 141 if accountDetails == nil || accountDetails.User == "" { 142 accountDetails = &jujuclient.AccountDetails{} 143 } else { 144 u := names.NewUserTag(accountDetails.User) 145 if !u.IsLocal() { 146 accountDetails = &jujuclient.AccountDetails{} 147 } 148 } 149 param, err := c.NewAPIConnectionParams( 150 store, controllerName, modelName, accountDetails, 151 ) 152 if err != nil { 153 return nil, errors.Trace(err) 154 } 155 conn, err := juju.NewAPIConnection(param) 156 if modelName != "" && params.ErrCode(err) == params.CodeModelNotFound { 157 return nil, c.missingModelError(store, controllerName, modelName) 158 } 159 return conn, err 160 } 161 162 // RemoveModelFromClientStore removes given model from client cache, store, 163 // for a given controller. 164 // If this model has also been cached as current, it will be reset if 165 // the requesting command can modify current model. 166 // For example, commands such as add/destroy-model, login/register, etc. 167 // If the model was cached as currnet but the command is not expected to 168 // change current model, this call will still remove model details from the client cache 169 // but will keep current model name intact to allow subsequent calls to try to resolve 170 // model details on the controller. 171 func (c *CommandBase) RemoveModelFromClientStore(store jujuclient.ClientStore, controllerName, modelName string) { 172 err := store.RemoveModel(controllerName, modelName) 173 if err != nil && !errors.IsNotFound(err) { 174 logger.Warningf("cannot remove unknown model from cache: %v", err) 175 } 176 if c.CanClearCurrentModel { 177 currentModel, err := store.CurrentModel(controllerName) 178 if err != nil { 179 logger.Warningf("cannot read current model: %v", err) 180 } else if currentModel == modelName { 181 if err := store.SetCurrentModel(controllerName, ""); err != nil { 182 logger.Warningf("cannot reset current model: %v", err) 183 } 184 } 185 } 186 } 187 188 func (c *CommandBase) missingModelError(store jujuclient.ClientStore, controllerName, modelName string) error { 189 // First, we'll try and clean up the missing model from the local cache. 190 c.RemoveModelFromClientStore(store, controllerName, modelName) 191 return errors.Errorf("model %q has been removed from the controller, run 'juju models' and switch to one of them.", modelName) 192 } 193 194 // NewAPIConnectionParams returns a juju.NewAPIConnectionParams with the 195 // given arguments such that a call to juju.NewAPIConnection with the 196 // result behaves the same as a call to CommandBase.NewAPIRoot with 197 // the same arguments. 198 func (c *CommandBase) NewAPIConnectionParams( 199 store jujuclient.ClientStore, 200 controllerName, modelName string, 201 accountDetails *jujuclient.AccountDetails, 202 ) (juju.NewAPIConnectionParams, error) { 203 c.assertRunStarted() 204 bakeryClient, err := c.BakeryClient(store, controllerName) 205 if err != nil { 206 return juju.NewAPIConnectionParams{}, errors.Trace(err) 207 } 208 var getPassword func(username string) (string, error) 209 if c.cmdContext != nil { 210 getPassword = func(username string) (string, error) { 211 fmt.Fprintf(c.cmdContext.Stderr, "please enter password for %s on %s: ", username, controllerName) 212 defer fmt.Fprintln(c.cmdContext.Stderr) 213 return readPassword(c.cmdContext.Stdin) 214 } 215 } else { 216 getPassword = func(username string) (string, error) { 217 return "", errors.New("no context to prompt for password") 218 } 219 } 220 221 return newAPIConnectionParams( 222 store, controllerName, modelName, 223 accountDetails, 224 bakeryClient, 225 c.apiOpen, 226 getPassword, 227 ) 228 } 229 230 // HTTPClient returns an http.Client that contains the loaded 231 // persistent cookie jar. Note that this client is not good for 232 // connecting to the Juju API itself because it does not 233 // have the correct TLS setup - use api.Connection.HTTPClient 234 // for that. 235 func (c *CommandBase) HTTPClient(store jujuclient.ClientStore, controllerName string) (*http.Client, error) { 236 c.assertRunStarted() 237 bakeryClient, err := c.BakeryClient(store, controllerName) 238 if err != nil { 239 return nil, errors.Trace(err) 240 } 241 return bakeryClient.Client, nil 242 } 243 244 // BakeryClient returns a macaroon bakery client that 245 // uses the same HTTP client returned by HTTPClient. 246 func (c *CommandBase) BakeryClient(store jujuclient.CookieStore, controllerName string) (*httpbakery.Client, error) { 247 c.assertRunStarted() 248 ctx, err := c.getAPIContext(store, controllerName) 249 if err != nil { 250 return nil, errors.Trace(err) 251 } 252 return ctx.NewBakeryClient(), nil 253 } 254 255 // APIOpen establishes a connection to the API server using the 256 // the given api.Info and api.DialOpts, and associating any stored 257 // authorization tokens with the given controller name. 258 func (c *CommandBase) APIOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 259 c.assertRunStarted() 260 return c.apiOpen(info, opts) 261 } 262 263 // apiOpen establishes a connection to the API server using the 264 // the give api.Info and api.DialOpts. 265 func (c *CommandBase) apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 266 if c.apiOpenFunc != nil { 267 return c.apiOpenFunc(info, opts) 268 } 269 return api.Open(info, opts) 270 } 271 272 // RefreshModels refreshes the local models cache for the current user 273 // on the specified controller. 274 func (c *CommandBase) RefreshModels(store jujuclient.ClientStore, controllerName string) error { 275 if c.refreshModels == nil { 276 return c.doRefreshModels(store, controllerName) 277 } 278 return c.refreshModels(store, controllerName) 279 } 280 281 func (c *CommandBase) doRefreshModels(store jujuclient.ClientStore, controllerName string) error { 282 c.assertRunStarted() 283 modelManager, err := c.modelAPI(store, controllerName) 284 if err != nil { 285 return errors.Trace(err) 286 } 287 defer modelManager.Close() 288 289 accountDetails, err := store.AccountDetails(controllerName) 290 if err != nil { 291 return errors.Trace(err) 292 } 293 294 models, err := modelManager.ListModels(accountDetails.User) 295 if err != nil { 296 return errors.Trace(err) 297 } 298 if err := c.SetControllerModels(store, controllerName, models); err != nil { 299 return errors.Trace(err) 300 } 301 return nil 302 } 303 304 func (c *CommandBase) SetControllerModels(store jujuclient.ClientStore, controllerName string, models []base.UserModel) error { 305 modelsToStore := make(map[string]jujuclient.ModelDetails, len(models)) 306 for _, model := range models { 307 modelDetails := jujuclient.ModelDetails{ModelUUID: model.UUID, ModelType: model.Type} 308 owner := names.NewUserTag(model.Owner) 309 modelName := jujuclient.JoinOwnerModelName(owner, model.Name) 310 modelsToStore[modelName] = modelDetails 311 } 312 if err := store.SetModels(controllerName, modelsToStore); err != nil { 313 return errors.Trace(err) 314 } 315 return nil 316 } 317 318 // ModelUUIDs returns the model UUIDs for the given model names. 319 func (c *CommandBase) ModelUUIDs(store jujuclient.ClientStore, controllerName string, modelNames []string) ([]string, error) { 320 var result []string 321 for _, modelName := range modelNames { 322 model, err := store.ModelByName(controllerName, modelName) 323 if errors.IsNotFound(err) { 324 // The model isn't known locally, so query the models available in the controller. 325 logger.Infof("model %q not cached locally, refreshing models from controller", modelName) 326 if err := c.RefreshModels(store, controllerName); err != nil { 327 return nil, errors.Annotatef(err, "refreshing model %q", modelName) 328 } 329 model, err = store.ModelByName(controllerName, modelName) 330 } 331 if err != nil { 332 return nil, errors.Trace(err) 333 } 334 result = append(result, model.ModelUUID) 335 } 336 return result, nil 337 } 338 339 // getAPIContext returns an apiContext for the given controller. 340 // It will return the same context if called twice for the same controller. 341 // The context will be closed when closeAPIContexts is called. 342 func (c *CommandBase) getAPIContext(store jujuclient.CookieStore, controllerName string) (*apiContext, error) { 343 c.assertRunStarted() 344 if ctx := c.apiContexts[controllerName]; ctx != nil { 345 return ctx, nil 346 } 347 if controllerName == "" { 348 return nil, errors.New("cannot get API context from empty controller name") 349 } 350 ctx, err := newAPIContext(c.cmdContext, &c.authOpts, store, controllerName) 351 if err != nil { 352 return nil, errors.Trace(err) 353 } 354 c.apiContexts[controllerName] = ctx 355 return ctx, nil 356 } 357 358 // CookieJar returns the cookie jar that is used to store auth credentials 359 // when connecting to the API. 360 func (c *CommandBase) CookieJar(store jujuclient.CookieStore, controllerName string) (http.CookieJar, error) { 361 ctx, err := c.getAPIContext(store, controllerName) 362 if err != nil { 363 return nil, errors.Trace(err) 364 } 365 return ctx.CookieJar(), nil 366 } 367 368 // ClearControllerMacaroons will remove all macaroons stored 369 // for the given controller from the persistent cookie jar. 370 // This is called both from 'juju logout' and a failed 'juju register'. 371 func (c *CommandBase) ClearControllerMacaroons(store jujuclient.CookieStore, controllerName string) error { 372 ctx, err := c.getAPIContext(store, controllerName) 373 if err != nil { 374 return errors.Trace(err) 375 } 376 ctx.jar.RemoveAll() 377 return nil 378 } 379 380 func (c *CommandBase) initContexts(ctx *cmd.Context) { 381 c.cmdContext = ctx 382 c.apiContexts = make(map[string]*apiContext) 383 } 384 385 // WrapBase wraps the specified Command. This should be 386 // used by any command that embeds CommandBase. 387 func WrapBase(c Command) Command { 388 return &baseCommandWrapper{ 389 Command: c, 390 } 391 } 392 393 type baseCommandWrapper struct { 394 Command 395 } 396 397 // inner implements wrapper.inner. 398 func (w *baseCommandWrapper) inner() cmd.Command { 399 return w.Command 400 } 401 402 // Run implements Command.Run. 403 func (w *baseCommandWrapper) Run(ctx *cmd.Context) error { 404 defer w.closeAPIContexts() 405 w.initContexts(ctx) 406 w.setRunStarted() 407 return w.Command.Run(ctx) 408 } 409 410 func newAPIConnectionParams( 411 store jujuclient.ClientStore, 412 controllerName, 413 modelName string, 414 accountDetails *jujuclient.AccountDetails, 415 bakery *httpbakery.Client, 416 apiOpen api.OpenFunc, 417 getPassword func(string) (string, error), 418 ) (juju.NewAPIConnectionParams, error) { 419 if controllerName == "" { 420 return juju.NewAPIConnectionParams{}, errors.Trace(errNoNameSpecified) 421 } 422 var modelUUID string 423 if modelName != "" { 424 modelDetails, err := store.ModelByName(controllerName, modelName) 425 if err != nil { 426 return juju.NewAPIConnectionParams{}, errors.Trace(err) 427 } 428 modelUUID = modelDetails.ModelUUID 429 } 430 dialOpts := api.DefaultDialOpts() 431 dialOpts.BakeryClient = bakery 432 433 if accountDetails != nil { 434 bakery.WebPageVisitor = httpbakery.NewMultiVisitor( 435 authentication.NewVisitor(accountDetails.User, getPassword), 436 bakery.WebPageVisitor, 437 ) 438 } 439 440 return juju.NewAPIConnectionParams{ 441 Store: store, 442 ControllerName: controllerName, 443 AccountDetails: accountDetails, 444 ModelUUID: modelUUID, 445 DialOpts: dialOpts, 446 OpenAPI: apiOpen, 447 }, nil 448 } 449 450 // NewGetBootstrapConfigParamsFunc returns a function that, given a controller name, 451 // returns the params needed to bootstrap a fresh copy of that controller in the given client store. 452 func NewGetBootstrapConfigParamsFunc( 453 ctx *cmd.Context, 454 store jujuclient.ClientStore, 455 providerRegistry environs.ProviderRegistry, 456 ) func(string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) { 457 return bootstrapConfigGetter{ctx, store, providerRegistry}.getBootstrapConfigParams 458 } 459 460 type bootstrapConfigGetter struct { 461 ctx *cmd.Context 462 store jujuclient.ClientStore 463 registry environs.ProviderRegistry 464 } 465 466 func (g bootstrapConfigGetter) getBootstrapConfigParams(controllerName string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) { 467 controllerDetails, err := g.store.ControllerByName(controllerName) 468 if err != nil { 469 return nil, nil, errors.Annotate(err, "resolving controller name") 470 } 471 bootstrapConfig, err := g.store.BootstrapConfigForController(controllerName) 472 if err != nil { 473 return nil, nil, errors.Annotate(err, "getting bootstrap config") 474 } 475 476 var credential *cloud.Credential 477 bootstrapCloud := cloud.Cloud{ 478 Name: bootstrapConfig.Cloud, 479 Type: bootstrapConfig.CloudType, 480 Endpoint: bootstrapConfig.CloudEndpoint, 481 IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint, 482 } 483 if bootstrapConfig.Credential != "" { 484 if bootstrapConfig.CloudRegion != "" { 485 bootstrapCloud.Regions = []cloud.Region{{ 486 Name: bootstrapConfig.CloudRegion, 487 Endpoint: bootstrapConfig.CloudEndpoint, 488 IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint, 489 }} 490 } 491 credential, _, _, err = GetCredentials( 492 g.ctx, g.store, 493 GetCredentialsParams{ 494 Cloud: bootstrapCloud, 495 CloudRegion: bootstrapConfig.CloudRegion, 496 CredentialName: bootstrapConfig.Credential, 497 }, 498 ) 499 if err != nil { 500 return nil, nil, errors.Trace(err) 501 } 502 } else { 503 // The credential was auto-detected; run auto-detection again. 504 provider, err := g.registry.Provider(bootstrapConfig.CloudType) 505 if err != nil { 506 return nil, nil, errors.Trace(err) 507 } 508 cloudCredential, err := DetectCredential(bootstrapConfig.Cloud, provider) 509 if err != nil { 510 return nil, nil, errors.Trace(err) 511 } 512 // DetectCredential ensures that there is only one credential 513 // to choose from. It's still in a map, though, hence for..range. 514 var credentialName string 515 for name, one := range cloudCredential.AuthCredentials { 516 credential = &one 517 credentialName = name 518 } 519 credential, err = FinalizeFileContent(credential, provider) 520 if err != nil { 521 return nil, nil, AnnotateWithFinalizationError(err, credentialName, bootstrapCloud.Name) 522 } 523 credential, err = provider.FinalizeCredential( 524 g.ctx, environs.FinalizeCredentialParams{ 525 Credential: *credential, 526 CloudEndpoint: bootstrapConfig.CloudEndpoint, 527 CloudStorageEndpoint: bootstrapConfig.CloudStorageEndpoint, 528 CloudIdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint, 529 }, 530 ) 531 if err != nil { 532 return nil, nil, errors.Trace(err) 533 } 534 } 535 536 // Add attributes from the controller details. 537 538 // TODO(wallyworld) - remove after beta18 539 controllerModelUUID := bootstrapConfig.ControllerModelUUID 540 if controllerModelUUID == "" { 541 controllerModelUUID = controllerDetails.ControllerUUID 542 } 543 544 bootstrapConfig.Config[config.UUIDKey] = controllerModelUUID 545 cfg, err := config.New(config.NoDefaults, bootstrapConfig.Config) 546 if err != nil { 547 return nil, nil, errors.Trace(err) 548 } 549 return bootstrapConfig, &environs.PrepareConfigParams{ 550 Cloud: environs.CloudSpec{ 551 Type: bootstrapConfig.CloudType, 552 Name: bootstrapConfig.Cloud, 553 Region: bootstrapConfig.CloudRegion, 554 Endpoint: bootstrapConfig.CloudEndpoint, 555 IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint, 556 StorageEndpoint: bootstrapConfig.CloudStorageEndpoint, 557 Credential: credential, 558 CACertificates: bootstrapConfig.CloudCACertificates, 559 }, 560 Config: cfg, 561 }, nil 562 } 563 564 // TODO(axw) this is now in three places: change-password, 565 // register, and here. Refactor and move to a common location. 566 func readPassword(stdin io.Reader) (string, error) { 567 if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) { 568 password, err := terminal.ReadPassword(int(f.Fd())) 569 return string(password), err 570 } 571 return readLine(stdin) 572 } 573 574 func readLine(stdin io.Reader) (string, error) { 575 // Read one byte at a time to avoid reading beyond the delimiter. 576 line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n') 577 if err != nil { 578 return "", errors.Trace(err) 579 } 580 return line[:len(line)-1], nil 581 } 582 583 type byteAtATimeReader struct { 584 io.Reader 585 } 586 587 // Read is part of the io.Reader interface. 588 func (r byteAtATimeReader) Read(out []byte) (int, error) { 589 return r.Reader.Read(out[:1]) 590 } 591 592 // wrapper is implemented by types that wrap a command. 593 type wrapper interface { 594 inner() cmd.Command 595 } 596 597 // InnerCommand returns the command that has been wrapped 598 // by one of the Wrap functions. This is useful for 599 // tests that wish to inspect internal details of a command 600 // instance. If c isn't wrapping anything, it returns c. 601 func InnerCommand(c cmd.Command) cmd.Command { 602 for { 603 c1, ok := c.(wrapper) 604 if !ok { 605 return c 606 } 607 c = c1.inner() 608 } 609 }