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

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  package model
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/gnuflag"
    16  	"gopkg.in/juju/names.v2"
    17  
    18  	"github.com/juju/juju/api"
    19  	"github.com/juju/juju/api/base"
    20  	cloudapi "github.com/juju/juju/api/cloud"
    21  	"github.com/juju/juju/api/modelmanager"
    22  	jujucloud "github.com/juju/juju/cloud"
    23  	jujucmd "github.com/juju/juju/cmd"
    24  	"github.com/juju/juju/cmd/juju/block"
    25  	"github.com/juju/juju/cmd/juju/common"
    26  	"github.com/juju/juju/cmd/modelcmd"
    27  	"github.com/juju/juju/cmd/output"
    28  	"github.com/juju/juju/environs/config"
    29  )
    30  
    31  const (
    32  	modelDefaultsSummary = `Displays or sets default configuration settings for a model.`
    33  	modelDefaultsHelpDoc = `
    34  By default, all default configuration (keys and values) are
    35  displayed if a key is not specified. Supplying key=value will set the
    36  supplied key to the supplied value. This can be repeated for multiple keys.
    37  You can also specify a yaml file containing key values.
    38  By default, the model is the current model.
    39  
    40  
    41  Examples:
    42      juju model-defaults
    43      juju model-defaults http-proxy
    44      juju model-defaults aws/us-east-1 http-proxy
    45      juju model-defaults us-east-1 http-proxy
    46      juju model-defaults -m mymodel type
    47      juju model-defaults ftp-proxy=10.0.0.1:8000
    48      juju model-defaults aws/us-east-1 ftp-proxy=10.0.0.1:8000
    49      juju model-defaults us-east-1 ftp-proxy=10.0.0.1:8000
    50      juju model-defaults us-east-1 ftp-proxy=10.0.0.1:8000 path/to/file.yaml
    51      juju model-defaults us-east-1 path/to/file.yaml    
    52      juju model-defaults -m othercontroller:mymodel default-series=yakkety test-mode=false
    53      juju model-defaults --reset default-series test-mode
    54      juju model-defaults aws/us-east-1 --reset http-proxy
    55      juju model-defaults us-east-1 --reset http-proxy
    56  
    57  See also:
    58      models
    59      model-config
    60  `
    61  )
    62  
    63  // NewDefaultsCommand wraps defaultsCommand with sane model settings.
    64  func NewDefaultsCommand() cmd.Command {
    65  	defaultsCmd := &defaultsCommand{
    66  		newCloudAPI: func(caller base.APICallCloser) cloudAPI {
    67  			return cloudapi.NewClient(caller)
    68  		},
    69  		newDefaultsAPI: func(caller base.APICallCloser) defaultsCommandAPI {
    70  			return modelmanager.NewClient(caller)
    71  		},
    72  	}
    73  	defaultsCmd.newAPIRoot = defaultsCmd.NewAPIRoot
    74  	return modelcmd.WrapController(defaultsCmd)
    75  }
    76  
    77  // defaultsCommand is compound command for accessing and setting attributes
    78  // related to default model configuration.
    79  type defaultsCommand struct {
    80  	out cmd.Output
    81  	modelcmd.ControllerCommandBase
    82  
    83  	newAPIRoot     func() (api.Connection, error)
    84  	newDefaultsAPI func(base.APICallCloser) defaultsCommandAPI
    85  	newCloudAPI    func(base.APICallCloser) cloudAPI
    86  
    87  	// args holds all the command-line arguments before
    88  	// they've been parsed.
    89  	args []string
    90  
    91  	action                func(defaultsCommandAPI, *cmd.Context) error // The function handling the input, set in Init.
    92  	key                   string
    93  	resetKeys             []string // Holds the keys to be reset once parsed.
    94  	cloudName, regionName string
    95  	reset                 []string // Holds the keys to be reset until parsed.
    96  	setOptions            common.ConfigFlag
    97  }
    98  
    99  // cloudAPI defines an API to be passed in for testing.
   100  type cloudAPI interface {
   101  	Close() error
   102  	DefaultCloud() (names.CloudTag, error)
   103  	Cloud(names.CloudTag) (jujucloud.Cloud, error)
   104  }
   105  
   106  // defaultsCommandAPI defines an API to be used during testing.
   107  type defaultsCommandAPI interface {
   108  	// Close closes the api connection.
   109  	Close() error
   110  
   111  	// ModelDefaults returns the default config values used when creating a new model.
   112  	ModelDefaults() (config.ModelDefaultAttributes, error)
   113  
   114  	// SetModelDefaults sets the default config values to use
   115  	// when creating new models.
   116  	SetModelDefaults(cloud, region string, config map[string]interface{}) error
   117  
   118  	// UnsetModelDefaults clears the default model
   119  	// configuration values.
   120  	UnsetModelDefaults(cloud, region string, keys ...string) error
   121  }
   122  
   123  // Info implements part of the cmd.Command interface.
   124  func (c *defaultsCommand) Info() *cmd.Info {
   125  	return jujucmd.Info(&cmd.Info{
   126  		Args:    "[[<cloud/>]<region> ]<model-key>[<=value>] ...]",
   127  		Doc:     modelDefaultsHelpDoc,
   128  		Name:    "model-defaults",
   129  		Purpose: modelDefaultsSummary,
   130  		Aliases: []string{"model-default"},
   131  	})
   132  }
   133  
   134  // SetFlags implements part of the cmd.Command interface.
   135  func (c *defaultsCommand) SetFlags(f *gnuflag.FlagSet) {
   136  	c.ControllerCommandBase.SetFlags(f)
   137  
   138  	c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{
   139  		"yaml":    cmd.FormatYaml,
   140  		"json":    cmd.FormatJson,
   141  		"tabular": formatDefaultConfigTabular,
   142  	})
   143  	f.Var(cmd.NewAppendStringsValue(&c.reset), "reset", "Reset the provided comma delimited keys")
   144  }
   145  
   146  // Init implements cmd.Command.Init.
   147  func (c *defaultsCommand) Init(args []string) error {
   148  	// There's no way of distinguishing a cloud name
   149  	// from a model configuration setting without contacting the
   150  	// API, but we aren't allowed to contact the API at Init time,
   151  	// so we defer parsing the arguments until Run is called.
   152  	c.args = args
   153  	return nil
   154  }
   155  
   156  // Run implements part of the cmd.Command interface.
   157  func (c *defaultsCommand) Run(ctx *cmd.Context) error {
   158  	if err := c.parseArgs(c.args); err != nil {
   159  		return errors.Trace(err)
   160  	}
   161  	root, err := c.newAPIRoot()
   162  	if err != nil {
   163  		return errors.Trace(err)
   164  	}
   165  	client := c.newDefaultsAPI(root)
   166  	if err != nil {
   167  		return errors.Trace(err)
   168  	}
   169  	defer client.Close()
   170  
   171  	if len(c.resetKeys) > 0 {
   172  		err := c.resetDefaults(client, ctx)
   173  		if err != nil {
   174  			// We return this error naked as it is almost certainly going to be
   175  			// cmd.ErrSilent and the cmd.Command framework expects that back
   176  			// from cmd.Run if the process is blocked.
   177  			return err
   178  		}
   179  	}
   180  	if c.action == nil {
   181  		// If we are reset only we end up here, only we've already done that.
   182  		return nil
   183  	}
   184  	return c.action(client, ctx)
   185  }
   186  
   187  // This needs to parse a command line invocation to reset and set, or get
   188  // model-default values. The arguments may be interspersed as demonstrated in
   189  // the examples.
   190  //
   191  // This sets foo=baz and unsets bar in aws/us-east-1
   192  //     juju model-defaults aws/us-east-1 foo=baz --reset bar
   193  //
   194  // If aws is the cloud of the current or specified controller -- specified by
   195  // -c somecontroller -- then the following would also be equivalent.
   196  //     juju model-defaults --reset bar us-east-1 foo=baz
   197  //
   198  // If one doesn't specify a cloud or region the command is still valid but for
   199  // setting the default on the controller:
   200  //     juju model-defaults foo=baz --reset bar
   201  //
   202  // Of course one can specify multiple keys to reset --reset a,b,c and one can
   203  // also specify multiple values to set a=b c=d e=f. I.e. comma separated for
   204  // resetting and space separated for setting. One may also only set or reset as
   205  // a singular action.
   206  //     juju model-defaults --reset foo
   207  //     juju model-defaults a=b c=d e=f
   208  //     juju model-defaults a=b c=d --reset e,f
   209  //
   210  // cloud/region may also be specified so above examples with that option might
   211  // be like the following invokation.
   212  //     juju model-defaults us-east-1 a=b c=d --reset e,f
   213  //
   214  // Finally one can also ask for the all the defaults or the defaults for one
   215  // specific setting. In this case specifying a region is not valid as
   216  // model-defaults shows the settings for a value at all locations that it has a
   217  // default set -- or at a minimum the default and  "-" for a controller with no
   218  // value set.
   219  //     juju model-defaults
   220  //     juju model-defaults no-proxy
   221  //
   222  // It is not valid to reset and get or to set and get values. It is also
   223  // neither valid to reset and set the same key, nor to set the same key to
   224  // different values in the same command.
   225  //
   226  // For those playing along that all means the first positional arg can be a
   227  // cloud/region, a region, a key=value to set, a key to get the settings for,
   228  // or empty. Other caveats are that one cannot set and reset a value for the
   229  // same key, that is to say keys to be mutated must be unique.
   230  //
   231  // Here we go...
   232  func (c *defaultsCommand) parseArgs(args []string) error {
   233  	var err error
   234  	//  If there's nothing to reset and no args we're returning everything. So
   235  	//  we short circuit immediately.
   236  	if len(args) == 0 && len(c.reset) == 0 {
   237  		c.action = c.getDefaults
   238  		return nil
   239  	}
   240  
   241  	// If there is an argument provided to reset, we turn it into a slice of
   242  	// strings and verify them. If there is one or more valid keys to reset and
   243  	// no other errors initializing the command, c.resetDefaults will be called
   244  	// in c.Run.
   245  	if err = c.parseResetKeys(); err != nil {
   246  		return errors.Trace(err)
   247  	}
   248  
   249  	// Look at the first positional arg and test to see if it is a valid
   250  	// optional specification of cloud/region or region. If it is then
   251  	// cloudName and regionName are set on the object and the positional args
   252  	// are returned without the first element. If it cannot be validated;
   253  	// cloudName and regionName are left empty and we get back the same args we
   254  	// passed in.
   255  	args, err = c.parseArgsForRegion(args)
   256  	if err != nil {
   257  		return errors.Trace(err)
   258  	}
   259  
   260  	// Remember we *might* have one less arg at this point if we chopped the
   261  	// first off because it was a valid cloud/region option.
   262  	wantSet := false
   263  	if len(args) > 0 {
   264  		lastArg := args[len(args)-1]
   265  		// We may have a config.yaml file
   266  		_, err := os.Stat(lastArg)
   267  		wantSet = err == nil || strings.Contains(lastArg, "=")
   268  	}
   269  
   270  	switch {
   271  	case wantSet:
   272  		// In the event that we are setting values, the final positional arg
   273  		// will always have an "=" in it. So if we see that we know we want to
   274  		// set args.
   275  		return c.handleSetArgs(args)
   276  	case len(args) == 0:
   277  		if len(c.resetKeys) == 0 {
   278  			// If there's no positional args and reset is not set then we're
   279  			// getting all attrs.
   280  			c.action = c.getDefaults
   281  			return nil
   282  		}
   283  		// Reset only.
   284  		return nil
   285  	case len(args) == 1:
   286  		// We want to get settings for the provided key.
   287  		return c.handleOneArg(args[0])
   288  	default: // case args > 1
   289  		// Specifying any non key=value positional args after a key=value pair
   290  		// is invalid input. So if we have more than one the input is almost
   291  		// certainly invalid, but in different possible ways.
   292  		return c.handleExtraArgs(args)
   293  	}
   294  }
   295  
   296  // parseResetKeys splits the keys provided to --reset after trimming any
   297  // leading or trailing comma. It then verifies that we haven't incorrectly
   298  // received any key=value pairs and finally sets the value(s) on c.resetKeys.
   299  func (c *defaultsCommand) parseResetKeys() error {
   300  	if len(c.reset) == 0 {
   301  		return nil
   302  	}
   303  	var resetKeys []string
   304  	for _, value := range c.reset {
   305  		keys := strings.Split(strings.Trim(value, ","), ",")
   306  		resetKeys = append(resetKeys, keys...)
   307  	}
   308  
   309  	for _, k := range resetKeys {
   310  		if k == config.AgentVersionKey {
   311  			return errors.Errorf("%q cannot be reset", config.AgentVersionKey)
   312  		}
   313  		if strings.Contains(k, "=") {
   314  			return errors.Errorf(
   315  				`--reset accepts a comma delimited set of keys "a,b,c", received: %q`, k)
   316  		}
   317  	}
   318  	c.resetKeys = resetKeys
   319  	return nil
   320  }
   321  
   322  // parseArgsForRegion parses args to check if the first arg is a region and
   323  // returns the appropriate remaining args.
   324  func (c *defaultsCommand) parseArgsForRegion(args []string) ([]string, error) {
   325  	var err error
   326  	if len(args) > 0 {
   327  		// determine if the first arg is cloud/region or region and return
   328  		// appropriate positional args.
   329  		args, err = c.parseCloudRegion(args)
   330  		if err != nil {
   331  			return nil, errors.Trace(err)
   332  		}
   333  	}
   334  	return args, nil
   335  }
   336  
   337  // parseCloudRegion examines args to see if the first arg is a cloud/region or
   338  // region. If not it returns the full args slice. If it is then it sets cloud
   339  // and/or region on the object and sends the remaining args back to the caller.
   340  func (c *defaultsCommand) parseCloudRegion(args []string) ([]string, error) {
   341  	var cloud, region string
   342  	cr := args[0]
   343  	// Must have no more than one slash and it must not be at the beginning or end.
   344  	if strings.Count(cr, "/") == 1 && !strings.HasPrefix(cr, "/") && !strings.HasSuffix(cr, "/") {
   345  		elems := strings.Split(cr, "/")
   346  		cloud, region = elems[0], elems[1]
   347  	} else {
   348  		region = cr
   349  	}
   350  
   351  	// TODO(redir) 2016-10-05 #1627162
   352  	// We don't disallow "=" in region names, but probably should.
   353  	if strings.Contains(region, "=") {
   354  		return args, nil
   355  	}
   356  
   357  	valid, err := c.validCloudRegion(cloud, region)
   358  	if err != nil {
   359  		return nil, errors.Trace(err)
   360  	}
   361  	if !valid {
   362  		return args, nil
   363  	}
   364  	return args[1:], nil
   365  }
   366  
   367  // validCloudRegion checks that region is a valid region in cloud, or default cloud
   368  // if cloud is not specified.
   369  func (c *defaultsCommand) validCloudRegion(cloudName, region string) (bool, error) {
   370  	var (
   371  		isCloudRegion bool
   372  		cloud         jujucloud.Cloud
   373  		cTag          names.CloudTag
   374  		err           error
   375  	)
   376  
   377  	root, err := c.newAPIRoot()
   378  	if err != nil {
   379  		return false, errors.Trace(err)
   380  	}
   381  	cc := c.newCloudAPI(root)
   382  	defer cc.Close()
   383  
   384  	if cloudName == "" {
   385  		cTag, err = cc.DefaultCloud()
   386  		if err != nil {
   387  			return false, errors.Trace(err)
   388  		}
   389  	} else {
   390  		if !names.IsValidCloud(cloudName) {
   391  			return false, errors.Errorf("invalid cloud %q", cloudName)
   392  		}
   393  		cTag = names.NewCloudTag(cloudName)
   394  	}
   395  	cloud, err = cc.Cloud(cTag)
   396  	if err != nil {
   397  		return false, errors.Trace(err)
   398  	}
   399  
   400  	for _, r := range cloud.Regions {
   401  		if r.Name == region {
   402  			c.cloudName = cTag.Id()
   403  			c.regionName = region
   404  			isCloudRegion = true
   405  			break
   406  		}
   407  	}
   408  	return isCloudRegion, nil
   409  }
   410  
   411  // handleSetArgs parses args for setting defaults.
   412  func (c *defaultsCommand) handleSetArgs(args []string) error {
   413  	// We may have a config.yaml file
   414  	_, err := os.Stat(args[0])
   415  	argZeroKeyOnly := err != nil && !strings.Contains(args[0], "=")
   416  	// If an invalid region was specified, the first positional arg won't have
   417  	// an "=". If we see one here we know it is invalid.
   418  	switch {
   419  	case argZeroKeyOnly && c.regionName == "":
   420  		return errors.Errorf("invalid region specified: %q", args[0])
   421  	case argZeroKeyOnly && c.regionName != "":
   422  		return errors.New("cannot set and retrieve default values simultaneously")
   423  	default:
   424  		if err := c.parseSetKeys(args); err != nil {
   425  			return errors.Trace(err)
   426  		}
   427  		c.action = c.setDefaults
   428  		return nil
   429  	}
   430  }
   431  
   432  // parseSetKeys iterates over the args and make sure that the key=value pairs
   433  // are valid. It also checks that the same key isn't also being reset.
   434  func (c *defaultsCommand) parseSetKeys(args []string) error {
   435  	for _, arg := range args {
   436  		if err := c.setOptions.Set(arg); err != nil {
   437  			return errors.Trace(err)
   438  		}
   439  	}
   440  	return nil
   441  }
   442  
   443  // handleOneArg handles the case where we have one positional arg after
   444  // processing for a region and the reset flag.
   445  func (c *defaultsCommand) handleOneArg(arg string) error {
   446  	resetSpecified := c.resetKeys != nil
   447  	regionSpecified := c.regionName != ""
   448  
   449  	if regionSpecified {
   450  		if resetSpecified {
   451  			// If a region was specified and reset was specified, we shouldn't have
   452  			// an extra arg. If it had an "=" in it, we should have handled it
   453  			// already.
   454  			return errors.New("cannot retrieve defaults for a region and reset attributes at the same time")
   455  		}
   456  	}
   457  	if resetSpecified {
   458  		// It makes no sense to supply a positional arg that isn't a region if
   459  		// we are resetting keys in a region, so we must have gotten an invalid
   460  		// region.
   461  		return errors.Errorf("invalid region specified: %q", arg)
   462  	}
   463  	// We can retrieve a value.
   464  	c.key = arg
   465  	c.action = c.getDefaults
   466  	return nil
   467  }
   468  
   469  // handleExtraArgs handles the case where too many args were supplied.
   470  func (c *defaultsCommand) handleExtraArgs(args []string) error {
   471  	resetSpecified := c.resetKeys != nil
   472  	regionSpecified := c.regionName != ""
   473  	numArgs := len(args)
   474  
   475  	// if we have a key=value pair here then something is wrong because the
   476  	// last positional arg is not one. We assume the user intended to get a
   477  	// value after setting them.
   478  	for _, arg := range args {
   479  		// We may have a config.yaml file
   480  		_, err := os.Stat(arg)
   481  		if err == nil || strings.Contains(arg, "=") {
   482  			return errors.New("cannot set and retrieve default values simultaneously")
   483  		}
   484  	}
   485  
   486  	if !regionSpecified {
   487  		if resetSpecified {
   488  			if numArgs == 2 {
   489  				// It makes no sense to supply a positional arg that isn't a
   490  				// region if we are resetting a region, so we must have gotten
   491  				// an invalid region.
   492  				return errors.Errorf("invalid region specified: %q", args[0])
   493  			}
   494  		}
   495  		if !resetSpecified {
   496  			// If we land here it is because there are extraneous positional
   497  			// args.
   498  			return errors.New("can only retrieve defaults for one key or all")
   499  		}
   500  	}
   501  	return errors.New("invalid input")
   502  }
   503  
   504  // getDefaults writes out the value for a single key or the full tree of
   505  // defaults.
   506  func (c *defaultsCommand) getDefaults(client defaultsCommandAPI, ctx *cmd.Context) error {
   507  	attrs, err := client.ModelDefaults()
   508  	if err != nil {
   509  		return err
   510  	}
   511  
   512  	valueForRegion := func(region string, regions []config.RegionDefaultValue) (config.RegionDefaultValue, bool) {
   513  		for _, r := range regions {
   514  			if r.Name == region {
   515  				return r, true
   516  			}
   517  		}
   518  		return config.RegionDefaultValue{}, false
   519  	}
   520  
   521  	// Filter by region if necessary.
   522  	if c.regionName != "" {
   523  		for attrName, attr := range attrs {
   524  			if regionDefault, ok := valueForRegion(c.regionName, attr.Regions); !ok {
   525  				delete(attrs, attrName)
   526  			} else {
   527  				attrForRegion := attr
   528  				attrForRegion.Regions = []config.RegionDefaultValue{regionDefault}
   529  				attrs[attrName] = attrForRegion
   530  			}
   531  		}
   532  	}
   533  
   534  	if c.key != "" {
   535  		if value, ok := attrs[c.key]; ok {
   536  			attrs = config.ModelDefaultAttributes{
   537  				c.key: value,
   538  			}
   539  		} else {
   540  			msg := fmt.Sprintf("there are no default model values for %q", c.key)
   541  			if c.regionName != "" {
   542  				msg += fmt.Sprintf(" in region %q", c.regionName)
   543  			}
   544  			return errors.New(msg)
   545  		}
   546  	}
   547  	if c.regionName != "" && len(attrs) == 0 {
   548  		return errors.New(fmt.Sprintf(
   549  			"there are no default model values in region %q", c.regionName))
   550  	}
   551  
   552  	// If c.keys is empty, write out the whole lot.
   553  	return c.out.Write(ctx, attrs)
   554  }
   555  
   556  // setDefaults sets defaults as provided in c.values.
   557  func (c *defaultsCommand) setDefaults(client defaultsCommandAPI, ctx *cmd.Context) error {
   558  	attrs, err := c.setOptions.ReadAttrs(ctx)
   559  	if err != nil {
   560  		return errors.Trace(err)
   561  	}
   562  	var keys []string
   563  	values := make(attributes)
   564  	for k, v := range attrs {
   565  		if k == config.AgentVersionKey {
   566  			return errors.Errorf(`"agent-version" must be set via "upgrade-model"`)
   567  		}
   568  		values[k] = v
   569  		keys = append(keys, k)
   570  	}
   571  
   572  	for _, k := range c.resetKeys {
   573  		if _, ok := values[k]; ok {
   574  			return errors.Errorf(
   575  				"key %q cannot be both set and unset in the same command", k)
   576  		}
   577  	}
   578  
   579  	if err := c.verifyKnownKeys(client, keys); err != nil {
   580  		return errors.Trace(err)
   581  	}
   582  	return block.ProcessBlockedError(
   583  		client.SetModelDefaults(
   584  			c.cloudName, c.regionName, values), block.BlockChange)
   585  }
   586  
   587  // resetDefaults resets the keys in resetKeys.
   588  func (c *defaultsCommand) resetDefaults(client defaultsCommandAPI, ctx *cmd.Context) error {
   589  	// ctx unused in this method.
   590  	if err := c.verifyKnownKeys(client, c.resetKeys); err != nil {
   591  		return errors.Trace(err)
   592  	}
   593  	return block.ProcessBlockedError(
   594  		client.UnsetModelDefaults(
   595  			c.cloudName, c.regionName, c.resetKeys...), block.BlockChange)
   596  
   597  }
   598  
   599  // verifyKnownKeys is a helper to validate the keys we are operating with
   600  // against the set of known attributes from the model.
   601  func (c *defaultsCommand) verifyKnownKeys(client defaultsCommandAPI, keys []string) error {
   602  	known, err := client.ModelDefaults()
   603  	if err != nil {
   604  		return errors.Trace(err)
   605  	}
   606  
   607  	allKeys := c.resetKeys[:]
   608  	for _, k := range keys {
   609  		allKeys = append(allKeys, k)
   610  	}
   611  
   612  	for _, key := range allKeys {
   613  		// check if the key exists in the known config
   614  		// and warn the user if the key is not defined
   615  		if _, exists := known[key]; !exists {
   616  			logger.Warningf(
   617  				"key %q is not defined in the known model configuration: possible misspelling", key)
   618  		}
   619  	}
   620  	return nil
   621  }
   622  
   623  // formatConfigTabular writes a tabular summary of default config information.
   624  func formatDefaultConfigTabular(writer io.Writer, value interface{}) error {
   625  	defaultValues, ok := value.(config.ModelDefaultAttributes)
   626  	if !ok {
   627  		return errors.Errorf("expected value of type %T, got %T", defaultValues, value)
   628  	}
   629  
   630  	tw := output.TabWriter(writer)
   631  	w := output.Wrapper{tw}
   632  
   633  	p := func(name string, value config.AttributeDefaultValues) {
   634  		var c, d interface{}
   635  		switch value.Default {
   636  		case nil:
   637  			d = "-"
   638  		case "":
   639  			d = `""`
   640  		default:
   641  			d = value.Default
   642  		}
   643  		switch value.Controller {
   644  		case nil:
   645  			c = "-"
   646  		case "":
   647  			c = `""`
   648  		default:
   649  			c = value.Controller
   650  		}
   651  		w.Println(name, d, c)
   652  		for _, region := range value.Regions {
   653  			w.Println("  "+region.Name, region.Value, "-")
   654  		}
   655  	}
   656  	var valueNames []string
   657  	for name := range defaultValues {
   658  		valueNames = append(valueNames, name)
   659  	}
   660  	sort.Strings(valueNames)
   661  
   662  	w.Println("Attribute", "Default", "Controller")
   663  
   664  	for _, name := range valueNames {
   665  		info := defaultValues[name]
   666  		out := &bytes.Buffer{}
   667  		err := cmd.FormatYaml(out, info)
   668  		if err != nil {
   669  			return errors.Annotatef(err, "formatting value for %q", name)
   670  		}
   671  		p(name, info)
   672  	}
   673  
   674  	tw.Flush()
   675  	return nil
   676  }