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  }