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

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package controller
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"crypto/rand"
    10  	"encoding/asn1"
    11  	"encoding/base64"
    12  	"encoding/json"
    13  	"fmt"
    14  	"io"
    15  	"net/http"
    16  	"os"
    17  	"strings"
    18  	"text/template"
    19  
    20  	"github.com/juju/cmd"
    21  	"github.com/juju/collections/set"
    22  	"github.com/juju/errors"
    23  	"github.com/juju/utils"
    24  	"golang.org/x/crypto/nacl/secretbox"
    25  	"golang.org/x/crypto/ssh/terminal"
    26  	"gopkg.in/juju/names.v2"
    27  
    28  	"github.com/juju/juju/api"
    29  	"github.com/juju/juju/api/base"
    30  	"github.com/juju/juju/api/modelmanager"
    31  	"github.com/juju/juju/apiserver/params"
    32  	jujucmd "github.com/juju/juju/cmd"
    33  	"github.com/juju/juju/cmd/juju/common"
    34  	"github.com/juju/juju/cmd/modelcmd"
    35  	"github.com/juju/juju/jujuclient"
    36  	"github.com/juju/juju/permission"
    37  )
    38  
    39  var noModelsMessage = `
    40  There are no models available. You can add models with
    41  "juju add-model", or you can ask an administrator or owner
    42  of a model to grant access to that model with "juju grant".
    43  `
    44  
    45  // NewRegisterCommand returns a command to allow the user to register a controller.
    46  func NewRegisterCommand() cmd.Command {
    47  	c := &registerCommand{}
    48  	c.apiOpen = c.APIOpen
    49  	c.listModelsFunc = c.listModels
    50  	c.store = jujuclient.NewFileClientStore()
    51  	c.CanClearCurrentModel = true
    52  	return modelcmd.WrapBase(c)
    53  }
    54  
    55  // registerCommand logs in to a Juju controller and caches the connection
    56  // information.
    57  type registerCommand struct {
    58  	modelcmd.CommandBase
    59  	apiOpen        api.OpenFunc
    60  	listModelsFunc func(_ jujuclient.ClientStore, controller, user string) ([]base.UserModel, error)
    61  	store          jujuclient.ClientStore
    62  	Arg            string
    63  
    64  	// onRunError is executed if non-nil if there is an error at the end
    65  	// of the Run method.
    66  	onRunError func()
    67  }
    68  
    69  var usageRegisterSummary = `
    70  Registers a controller.`[1:]
    71  
    72  var usageRegisterDetails = `
    73  The register command adds details of a controller to the local system.
    74  This is done either by completing the user registration process that
    75  began with the 'juju add-user' command, or by providing the DNS host
    76  name of a public controller.
    77  
    78  To complete the user registration process, you should have been provided
    79  with a base64-encoded blob of data (the output of 'juju add-user')
    80  which can be copied and pasted as the <string> argument to 'register'.
    81  You will be prompted for a password, which, once set, causes the
    82  registration string to be voided. In order to start using Juju the user
    83  can now either add a model or wait for a model to be shared with them.
    84  Some machine providers will require the user to be in possession of
    85  certain credentials in order to add a model.
    86  
    87  When adding a controller at a public address, authentication via some
    88  external third party (for example Ubuntu SSO) will be required, usually
    89  by using a web browser.
    90  
    91  Examples:
    92  
    93      juju register MFATA3JvZDAnExMxMDQuMTU0LjQyLjQ0OjE3MDcwExAxMC4xMjguMC4yOjE3MDcwBCBEFCaXerhNImkKKabuX5ULWf2Bp4AzPNJEbXVWgraLrAA=
    94  
    95      juju register public-controller.example.com
    96  
    97  See also: 
    98      add-user
    99      change-user-password
   100      unregister`
   101  
   102  // Info implements Command.Info
   103  // `register` may seem generic, but is seen as simple and without potential
   104  // naming collisions in any current or planned features.
   105  func (c *registerCommand) Info() *cmd.Info {
   106  	return jujucmd.Info(&cmd.Info{
   107  		Name:    "register",
   108  		Args:    "<registration string>|<controller host name>",
   109  		Purpose: usageRegisterSummary,
   110  		Doc:     usageRegisterDetails,
   111  	})
   112  }
   113  
   114  // SetFlags implements Command.Init.
   115  func (c *registerCommand) Init(args []string) error {
   116  	if len(args) < 1 {
   117  		return errors.New("registration data missing")
   118  	}
   119  	c.Arg, args = args[0], args[1:]
   120  	if err := cmd.CheckEmpty(args); err != nil {
   121  		return errors.Trace(err)
   122  	}
   123  	return nil
   124  }
   125  
   126  // Run implements Command.Run.
   127  func (c *registerCommand) Run(ctx *cmd.Context) error {
   128  	err := c.run(ctx)
   129  	if err != nil && c.onRunError != nil {
   130  		c.onRunError()
   131  	}
   132  	return err
   133  }
   134  
   135  func (c *registerCommand) run(ctx *cmd.Context) error {
   136  	c.store = modelcmd.QualifyingClientStore{c.store}
   137  	registrationParams, err := c.getParameters(ctx)
   138  	if err != nil {
   139  		return errors.Trace(err)
   140  	}
   141  	controllerName, err := c.promptControllerName(registrationParams.defaultControllerName, ctx.Stderr, ctx.Stdin)
   142  	if err != nil {
   143  		return errors.Trace(err)
   144  	}
   145  	controllerDetails, accountDetails, err := c.controllerDetails(ctx, registrationParams, controllerName)
   146  	if err != nil {
   147  		return errors.Trace(err)
   148  	}
   149  	if err := c.updateController(
   150  		ctx,
   151  		c.store,
   152  		controllerName,
   153  		controllerDetails,
   154  		accountDetails,
   155  	); err != nil {
   156  		return errors.Trace(err)
   157  	}
   158  	// Log into the controller to verify the credentials, and
   159  	// list the models available.
   160  	models, err := c.listModelsFunc(c.store, controllerName, accountDetails.User)
   161  	if err != nil {
   162  		return errors.Trace(err)
   163  	}
   164  	if err := c.SetControllerModels(c.store, controllerName, models); err != nil {
   165  		return errors.Annotate(err, "storing model details")
   166  	}
   167  	if err := c.store.SetCurrentController(controllerName); err != nil {
   168  		return errors.Trace(err)
   169  	}
   170  
   171  	fmt.Fprintf(
   172  		ctx.Stderr, "\nWelcome, %s. You are now logged into %q.\n",
   173  		friendlyUserName(accountDetails.User), controllerName,
   174  	)
   175  	return c.maybeSetCurrentModel(ctx, c.store, controllerName, accountDetails.User, models)
   176  }
   177  
   178  func friendlyUserName(user string) string {
   179  	u := names.NewUserTag(user)
   180  	if u.IsLocal() {
   181  		return u.Name()
   182  	}
   183  	return u.Id()
   184  }
   185  
   186  // controllerDetails returns controller and account details to be registered for the
   187  // given registration parameters.
   188  func (c *registerCommand) controllerDetails(ctx *cmd.Context, p *registrationParams, controllerName string) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   189  	if p.publicHost != "" {
   190  		return c.publicControllerDetails(p.publicHost, controllerName)
   191  	}
   192  	return c.nonPublicControllerDetails(ctx, p, controllerName)
   193  }
   194  
   195  // publicControllerDetails returns controller and account details to be registered
   196  // for the given public controller host name.
   197  func (c *registerCommand) publicControllerDetails(host, controllerName string) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   198  	errRet := func(err error) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   199  		return jujuclient.ControllerDetails{}, jujuclient.AccountDetails{}, err
   200  	}
   201  	apiAddr := host
   202  	if !strings.Contains(apiAddr, ":") {
   203  		apiAddr += ":443"
   204  	}
   205  	// Make a direct API connection because we don't yet know the
   206  	// controller UUID so can't store the thus-incomplete controller
   207  	// details to make a conventional connection.
   208  	//
   209  	// Unfortunately this means we'll connect twice to the controller
   210  	// but it's probably best to go through the conventional path the
   211  	// second time.
   212  	bclient, err := c.BakeryClient(c.store, controllerName)
   213  	if err != nil {
   214  		return errRet(errors.Trace(err))
   215  	}
   216  	dialOpts := api.DefaultDialOpts()
   217  	dialOpts.BakeryClient = bclient
   218  	conn, err := c.apiOpen(&api.Info{
   219  		Addrs: []string{apiAddr},
   220  	}, dialOpts)
   221  	if err != nil {
   222  		return errRet(errors.Trace(err))
   223  	}
   224  	defer conn.Close()
   225  	user, ok := conn.AuthTag().(names.UserTag)
   226  	if !ok {
   227  		return errRet(errors.Errorf("logged in as %v, not a user", conn.AuthTag()))
   228  	}
   229  	// If we get to here, then we have a cached macaroon for the registered
   230  	// user. If we encounter an error after here, we need to clear it.
   231  	c.onRunError = func() {
   232  		if err := c.ClearControllerMacaroons(c.store, controllerName); err != nil {
   233  			logger.Errorf("failed to clear macaroon: %v", err)
   234  		}
   235  	}
   236  	return jujuclient.ControllerDetails{
   237  			APIEndpoints:   []string{apiAddr},
   238  			ControllerUUID: conn.ControllerTag().Id(),
   239  		}, jujuclient.AccountDetails{
   240  			User:            user.Id(),
   241  			LastKnownAccess: conn.ControllerAccess(),
   242  		}, nil
   243  }
   244  
   245  // nonPublicControllerDetails returns controller and account details to be registered with
   246  // respect to the given registration parameters.
   247  func (c *registerCommand) nonPublicControllerDetails(ctx *cmd.Context, registrationParams *registrationParams, controllerName string) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   248  	errRet := func(err error) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) {
   249  		return jujuclient.ControllerDetails{}, jujuclient.AccountDetails{}, err
   250  	}
   251  	// During registration we must set a new password. This has to be done
   252  	// atomically with the clearing of the secret key.
   253  	payloadBytes, err := json.Marshal(params.SecretKeyLoginRequestPayload{
   254  		registrationParams.newPassword,
   255  	})
   256  	if err != nil {
   257  		return errRet(errors.Trace(err))
   258  	}
   259  
   260  	// Make the registration call. If this is successful, the client's
   261  	// cookie jar will be populated with a macaroon that may be used
   262  	// to log in below without the user having to type in the password
   263  	// again.
   264  	req := params.SecretKeyLoginRequest{
   265  		Nonce: registrationParams.nonce[:],
   266  		User:  registrationParams.userTag.String(),
   267  		PayloadCiphertext: secretbox.Seal(
   268  			nil, payloadBytes,
   269  			&registrationParams.nonce,
   270  			&registrationParams.key,
   271  		),
   272  	}
   273  	resp, err := c.secretKeyLogin(registrationParams.controllerAddrs, req, controllerName)
   274  	if err != nil {
   275  		// If we got here and got an error, the registration token supplied
   276  		// will be expired.
   277  		// Log the error as it will be useful for debugging, but give user a
   278  		// suggestion for the way forward instead of error details.
   279  		logger.Infof("while validating secret key: %v", err)
   280  		err = errors.Errorf("Provided registration token may have been expired.\nA controller administrator must reset your user to issue a new token.\nSee %q for more information.", "juju help change-user-password")
   281  		return errRet(errors.Trace(err))
   282  	}
   283  
   284  	// Decrypt the response to authenticate the controller and
   285  	// obtain its CA certificate.
   286  	if len(resp.Nonce) != len(registrationParams.nonce) {
   287  		return errRet(errors.NotValidf("response nonce"))
   288  	}
   289  	var respNonce [24]byte
   290  	copy(respNonce[:], resp.Nonce)
   291  	payloadBytes, ok := secretbox.Open(nil, resp.PayloadCiphertext, &respNonce, &registrationParams.key)
   292  	if !ok {
   293  		return errRet(errors.NotValidf("response payload"))
   294  	}
   295  	var responsePayload params.SecretKeyLoginResponsePayload
   296  	if err := json.Unmarshal(payloadBytes, &responsePayload); err != nil {
   297  		return errRet(errors.Annotate(err, "unmarshalling response payload"))
   298  	}
   299  	user := registrationParams.userTag.Id()
   300  	ctx.Infof("Initial password successfully set for %s.", friendlyUserName(user))
   301  	// If we get to here, then we have a cached macaroon for the registered
   302  	// user. If we encounter an error after here, we need to clear it.
   303  	c.onRunError = func() {
   304  		if err := c.ClearControllerMacaroons(c.store, controllerName); err != nil {
   305  			logger.Errorf("failed to clear macaroon: %v", err)
   306  		}
   307  	}
   308  	return jujuclient.ControllerDetails{
   309  			APIEndpoints:   registrationParams.controllerAddrs,
   310  			ControllerUUID: responsePayload.ControllerUUID,
   311  			CACert:         responsePayload.CACert,
   312  		}, jujuclient.AccountDetails{
   313  			User:            user,
   314  			LastKnownAccess: string(permission.LoginAccess),
   315  		}, nil
   316  }
   317  
   318  // updateController prompts for a controller name and updates the
   319  // controller and account details in the given client store.
   320  func (c *registerCommand) updateController(
   321  	ctx *cmd.Context,
   322  	store jujuclient.ClientStore,
   323  	controllerName string,
   324  	controllerDetails jujuclient.ControllerDetails,
   325  	accountDetails jujuclient.AccountDetails,
   326  ) error {
   327  	// Check that the same controller isn't already stored, so that we
   328  	// can avoid needlessly asking for a controller name in that case.
   329  	all, err := store.AllControllers()
   330  	if err != nil {
   331  		return errors.Trace(err)
   332  	}
   333  	for name, ctl := range all {
   334  		if ctl.ControllerUUID == controllerDetails.ControllerUUID {
   335  			var buf bytes.Buffer
   336  			if err := alreadyRegisteredMessageT.Execute(
   337  				&buf,
   338  				map[string]interface{}{
   339  					"ControllerName": name,
   340  					"UserName":       accountDetails.User,
   341  				},
   342  			); err != nil {
   343  				return err
   344  			}
   345  			ctx.Warningf(buf.String())
   346  			return errors.Errorf("controller is already registered as %q", name)
   347  		}
   348  	}
   349  	if err := store.AddController(controllerName, controllerDetails); err != nil {
   350  		return errors.Trace(err)
   351  	}
   352  	if err := store.UpdateAccount(controllerName, accountDetails); err != nil {
   353  		return errors.Annotatef(err, "cannot update account information: %v", err)
   354  	}
   355  	return nil
   356  }
   357  
   358  var alreadyRegisteredMessageT = template.Must(template.New("").Parse(`
   359  This controller has already been registered on this client as "{{.ControllerName}}."
   360  To login user "{{.UserName}}" run 'juju login -u {{.UserName}} -c {{.ControllerName}}'.
   361  To update controller details and login as user "{{.UserName}}":
   362      1. run 'juju unregister {{.UserName}}'
   363      2. request from your controller admin another registration string, i.e
   364         output from 'juju change-user-password {{.UserName}} --reset'
   365      3. re-run 'juju register' with the registration from (2) above.
   366  `[1:]))
   367  
   368  func (c *registerCommand) listModels(store jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) {
   369  	api, err := c.NewAPIRoot(store, controllerName, "")
   370  	if err != nil {
   371  		return nil, errors.Trace(err)
   372  	}
   373  	defer api.Close()
   374  	mm := modelmanager.NewClient(api)
   375  	return mm.ListModels(userName)
   376  }
   377  
   378  func (c *registerCommand) maybeSetCurrentModel(ctx *cmd.Context, store jujuclient.ClientStore, controllerName, userName string, models []base.UserModel) error {
   379  	if len(models) == 0 {
   380  		fmt.Fprint(ctx.Stderr, noModelsMessage)
   381  		return nil
   382  	}
   383  
   384  	// If we get to here, there is at least one model.
   385  	if len(models) == 1 {
   386  		// There is exactly one model shared,
   387  		// so set it as the current model.
   388  		model := models[0]
   389  		owner := names.NewUserTag(model.Owner)
   390  		modelName := jujuclient.JoinOwnerModelName(owner, model.Name)
   391  		err := store.SetCurrentModel(controllerName, modelName)
   392  		if err != nil {
   393  			return errors.Trace(err)
   394  		}
   395  		fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q.\n", modelName)
   396  		return nil
   397  	}
   398  	fmt.Fprintf(ctx.Stderr, `
   399  There are %d models available. Use "juju switch" to select
   400  one of them:
   401  `, len(models))
   402  	user := names.NewUserTag(userName)
   403  	ownerModelNames := make(set.Strings)
   404  	otherModelNames := make(set.Strings)
   405  	for _, model := range models {
   406  		if model.Owner == userName {
   407  			ownerModelNames.Add(model.Name)
   408  			continue
   409  		}
   410  		owner := names.NewUserTag(model.Owner)
   411  		modelName := common.OwnerQualifiedModelName(model.Name, owner, user)
   412  		otherModelNames.Add(modelName)
   413  	}
   414  	for _, modelName := range ownerModelNames.SortedValues() {
   415  		fmt.Fprintf(ctx.Stderr, "  - juju switch %s\n", modelName)
   416  	}
   417  	for _, modelName := range otherModelNames.SortedValues() {
   418  		fmt.Fprintf(ctx.Stderr, "  - juju switch %s\n", modelName)
   419  	}
   420  	return nil
   421  }
   422  
   423  type registrationParams struct {
   424  	// publicHost holds the host name of a public controller.
   425  	// If this is set, all other fields will be empty.
   426  	publicHost string
   427  
   428  	defaultControllerName string
   429  	userTag               names.UserTag
   430  	controllerAddrs       []string
   431  	key                   [32]byte
   432  	nonce                 [24]byte
   433  	newPassword           string
   434  }
   435  
   436  // getParameters gets all of the parameters required for registering, prompting
   437  // the user as necessary.
   438  func (c *registerCommand) getParameters(ctx *cmd.Context) (*registrationParams, error) {
   439  	var params registrationParams
   440  	if strings.Contains(c.Arg, ".") || c.Arg == "localhost" {
   441  		// Looks like a host name - no URL-encoded base64 string should
   442  		// contain a dot and every public controller name should.
   443  		// Allow localhost for development purposes.
   444  		params.publicHost = c.Arg
   445  		// No need for password shenanigans if we're using a public controller.
   446  		return &params, nil
   447  	}
   448  	// Decode key, username, controller addresses from the string supplied
   449  	// on the command line.
   450  	decodedData, err := base64.URLEncoding.DecodeString(c.Arg)
   451  	if err != nil {
   452  		return nil, errors.Trace(err)
   453  	}
   454  	var info jujuclient.RegistrationInfo
   455  	if _, err := asn1.Unmarshal(decodedData, &info); err != nil {
   456  		return nil, errors.Trace(err)
   457  	}
   458  
   459  	params.controllerAddrs = info.Addrs
   460  	params.userTag = names.NewUserTag(info.User)
   461  	if len(info.SecretKey) != len(params.key) {
   462  		return nil, errors.NotValidf("secret key")
   463  	}
   464  	copy(params.key[:], info.SecretKey)
   465  	params.defaultControllerName = info.ControllerName
   466  
   467  	// Prompt the user for the new password to set.
   468  	newPassword, err := c.promptNewPassword(ctx.Stderr, ctx.Stdin)
   469  	if err != nil {
   470  		return nil, errors.Trace(err)
   471  	}
   472  	params.newPassword = newPassword
   473  
   474  	// Generate a random nonce for encrypting the request.
   475  	if _, err := rand.Read(params.nonce[:]); err != nil {
   476  		return nil, errors.Trace(err)
   477  	}
   478  
   479  	return &params, nil
   480  }
   481  
   482  func (c *registerCommand) secretKeyLogin(addrs []string, request params.SecretKeyLoginRequest, controllerName string) (*params.SecretKeyLoginResponse, error) {
   483  	cookieJar, err := c.CookieJar(c.store, controllerName)
   484  	if err != nil {
   485  		return nil, errors.Annotate(err, "getting API context")
   486  	}
   487  
   488  	buf, err := json.Marshal(&request)
   489  	if err != nil {
   490  		return nil, errors.Annotate(err, "marshalling request")
   491  	}
   492  	r := bytes.NewReader(buf)
   493  
   494  	// Determine which address to use by attempting to open an API
   495  	// connection with each of the addresses. Note that we do not
   496  	// know the CA certificate yet, so we do not want to send any
   497  	// sensitive information. We make no attempt to log in until
   498  	// we can verify the server's identity.
   499  	opts := api.DefaultDialOpts()
   500  	opts.InsecureSkipVerify = true
   501  	conn, err := c.apiOpen(&api.Info{
   502  		Addrs:     addrs,
   503  		SkipLogin: true,
   504  	}, opts)
   505  	if err != nil {
   506  		return nil, errors.Trace(err)
   507  	}
   508  	apiAddr := conn.Addr()
   509  	if err := conn.Close(); err != nil {
   510  		return nil, errors.Trace(err)
   511  	}
   512  
   513  	// Using the address we connected to above, perform the request.
   514  	// A success response will include a macaroon cookie that we can
   515  	// use to log in with.
   516  	urlString := fmt.Sprintf("https://%s/register", apiAddr)
   517  	httpReq, err := http.NewRequest("POST", urlString, r)
   518  	if err != nil {
   519  		return nil, errors.Trace(err)
   520  	}
   521  	httpReq.Header.Set("Content-Type", "application/json")
   522  	httpClient := utils.GetNonValidatingHTTPClient()
   523  	httpClient.Jar = cookieJar
   524  	httpResp, err := httpClient.Do(httpReq)
   525  	if err != nil {
   526  		return nil, errors.Trace(err)
   527  	}
   528  	defer httpResp.Body.Close()
   529  
   530  	if httpResp.StatusCode != http.StatusOK {
   531  		var resp params.ErrorResult
   532  		if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
   533  			return nil, errors.Trace(err)
   534  		}
   535  		return nil, resp.Error
   536  	}
   537  
   538  	var resp params.SecretKeyLoginResponse
   539  	if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
   540  		return nil, errors.Annotatef(err, "cannot decode login response")
   541  	}
   542  	return &resp, nil
   543  }
   544  
   545  func (c *registerCommand) promptNewPassword(stderr io.Writer, stdin io.Reader) (string, error) {
   546  	password, err := c.readPassword("Enter a new password: ", stderr, stdin)
   547  	if err != nil {
   548  		return "", errors.Annotatef(err, "cannot read password")
   549  	}
   550  	if password == "" {
   551  		return "", errors.NewNotValid(nil, "you must specify a non-empty password")
   552  	}
   553  	passwordConfirmation, err := c.readPassword("Confirm password: ", stderr, stdin)
   554  	if err != nil {
   555  		return "", errors.Trace(err)
   556  	}
   557  	if password != passwordConfirmation {
   558  		return "", errors.Errorf("passwords do not match")
   559  	}
   560  	return password, nil
   561  }
   562  
   563  func (c *registerCommand) promptControllerName(suggestedName string, stderr io.Writer, stdin io.Reader) (string, error) {
   564  	if suggestedName != "" {
   565  		if _, err := c.store.ControllerByName(suggestedName); err == nil {
   566  			suggestedName = ""
   567  		}
   568  	}
   569  	for {
   570  		var setMsg string
   571  		setMsg = "Enter a name for this controller: "
   572  		if suggestedName != "" {
   573  			setMsg = fmt.Sprintf("Enter a name for this controller [%s]: ", suggestedName)
   574  		}
   575  		fmt.Fprintf(stderr, setMsg)
   576  		name, err := c.readLine(stdin)
   577  		if err != nil {
   578  			return "", errors.Trace(err)
   579  		}
   580  		name = strings.TrimSpace(name)
   581  		if name == "" {
   582  			if suggestedName == "" {
   583  				fmt.Fprintln(stderr, "You must specify a non-empty controller name.")
   584  				continue
   585  			}
   586  			name = suggestedName
   587  		}
   588  		_, err = c.store.ControllerByName(name)
   589  		if err == nil {
   590  			fmt.Fprintf(stderr, "Controller %q already exists.\n", name)
   591  			continue
   592  		}
   593  		return name, nil
   594  	}
   595  }
   596  
   597  func (c *registerCommand) readPassword(prompt string, stderr io.Writer, stdin io.Reader) (string, error) {
   598  	fmt.Fprintf(stderr, "%s", prompt)
   599  	defer stderr.Write([]byte{'\n'})
   600  	if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
   601  		password, err := terminal.ReadPassword(int(f.Fd()))
   602  		if err != nil {
   603  			return "", errors.Trace(err)
   604  		}
   605  		return string(password), nil
   606  	}
   607  	return c.readLine(stdin)
   608  }
   609  
   610  func (c *registerCommand) readLine(stdin io.Reader) (string, error) {
   611  	// Read one byte at a time to avoid reading beyond the delimiter.
   612  	line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n')
   613  	if err != nil {
   614  		return "", errors.Trace(err)
   615  	}
   616  	return line[:len(line)-1], nil
   617  }
   618  
   619  type byteAtATimeReader struct {
   620  	io.Reader
   621  }
   622  
   623  func (r byteAtATimeReader) Read(out []byte) (int, error) {
   624  	return r.Reader.Read(out[:1])
   625  }