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