github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/model/configcommand.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  	"io"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/juju/cmd"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/gnuflag"
    14  	"github.com/juju/utils/keyvalues"
    15  
    16  	"github.com/juju/juju/api/modelconfig"
    17  	"github.com/juju/juju/cmd/juju/block"
    18  	"github.com/juju/juju/cmd/modelcmd"
    19  	"github.com/juju/juju/cmd/output"
    20  	"github.com/juju/juju/environs/config"
    21  )
    22  
    23  const (
    24  	modelConfigSummary = "Displays or sets configuration values on a model."
    25  	modelConfigHelpDoc = `
    26  By default, all configuration (keys, source, and values) for the current model
    27  are displayed.
    28  
    29  Supplying one key name returns only the value for the key. Supplying key=value
    30  will set the supplied key to the supplied value, this can be repeated for
    31  multiple keys.
    32  
    33  Examples
    34      juju model-config default-series
    35      juju model-config -m mycontroller:mymodel
    36      juju model-config ftp-proxy=10.0.0.1:8000
    37      juju model-config -m othercontroller:mymodel default-series=yakkety test-mode=false
    38      juju model-config --reset default-series test-mode
    39  
    40  See also:
    41      models
    42      model-defaults
    43  `
    44  )
    45  
    46  // NewConfigCommand wraps configCommand with sane model settings.
    47  func NewConfigCommand() cmd.Command {
    48  	return modelcmd.Wrap(&configCommand{})
    49  }
    50  
    51  type attributes map[string]interface{}
    52  
    53  // configCommand is the simplified command for accessing and setting
    54  // attributes related to model configuration.
    55  type configCommand struct {
    56  	api configCommandAPI
    57  	modelcmd.ModelCommandBase
    58  	out cmd.Output
    59  
    60  	action    func(configCommandAPI, *cmd.Context) error // The action which we want to handle, set in cmd.Init.
    61  	keys      []string
    62  	reset     []string // Holds the keys to be reset until parsed.
    63  	resetKeys []string // Holds the keys to be reset once parsed.
    64  	values    attributes
    65  }
    66  
    67  // configCommandAPI defines an API interface to be used during testing.
    68  type configCommandAPI interface {
    69  	Close() error
    70  	ModelGet() (map[string]interface{}, error)
    71  	ModelGetWithMetadata() (config.ConfigValues, error)
    72  	ModelSet(config map[string]interface{}) error
    73  	ModelUnset(keys ...string) error
    74  }
    75  
    76  // Info implements part of the cmd.Command interface.
    77  func (c *configCommand) Info() *cmd.Info {
    78  	return &cmd.Info{
    79  		Args:    "[<model-key>[<=value>] ...]",
    80  		Doc:     modelConfigHelpDoc,
    81  		Name:    "model-config",
    82  		Purpose: modelConfigSummary,
    83  	}
    84  }
    85  
    86  // SetFlags implements part of the cmd.Command interface.
    87  func (c *configCommand) SetFlags(f *gnuflag.FlagSet) {
    88  	c.ModelCommandBase.SetFlags(f)
    89  
    90  	c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{
    91  		"json":    cmd.FormatJson,
    92  		"tabular": formatConfigTabular,
    93  		"yaml":    cmd.FormatYaml,
    94  	})
    95  	f.Var(cmd.NewAppendStringsValue(&c.reset), "reset", "Reset the provided comma delimited keys")
    96  }
    97  
    98  // Init implements part of the cmd.Command interface.
    99  func (c *configCommand) Init(args []string) error {
   100  	// If there are arguments provided to reset, we turn it into a slice of
   101  	// strings and verify them. If there is one or more valid keys to reset and
   102  	// no other errors initalizing the command, c.resetDefaults will be called
   103  	// in c.Run.
   104  	if err := c.parseResetKeys(); err != nil {
   105  		return errors.Trace(err)
   106  	}
   107  
   108  	switch len(args) {
   109  	case 0:
   110  		return c.handleZeroArgs()
   111  	case 1:
   112  		return c.handleOneArg(args[0])
   113  	default:
   114  		return c.handleArgs(args)
   115  	}
   116  }
   117  
   118  // handleZeroArgs handles the case where there are no positional args.
   119  func (c *configCommand) handleZeroArgs() error {
   120  	// If reset is empty we're getting configuration
   121  	if len(c.reset) == 0 {
   122  		c.action = c.getConfig
   123  	}
   124  	// Otherwise we're going to reset args.
   125  	return nil
   126  }
   127  
   128  // handleOneArg handles the case where there is one positional arg.
   129  func (c *configCommand) handleOneArg(arg string) error {
   130  	if strings.Contains(arg, "=") {
   131  		return c.parseSetKeys([]string{arg})
   132  	}
   133  	// If we are not setting a value, then we are retrieving one so we need to
   134  	// make sure that we are not resetting because it is not valid to get and
   135  	// reset simultaneously.
   136  	if len(c.reset) > 0 {
   137  		return errors.New("cannot set and retrieve model values simultaneously")
   138  	}
   139  	c.keys = []string{arg}
   140  	c.action = c.getConfig
   141  	return nil
   142  
   143  }
   144  
   145  // handleArgs handles the case where there's more than one positional arg.
   146  func (c *configCommand) handleArgs(args []string) error {
   147  	err := c.parseSetKeys(args)
   148  	if err != nil {
   149  		if !strings.Contains(strings.Join(args, " "), "=") {
   150  			return errors.New("can only retrieve a single value, or all values")
   151  		}
   152  		return errors.Trace(err)
   153  	}
   154  	return nil
   155  }
   156  
   157  // parseSetKeys iterates over the args and make sure that the key=value pairs
   158  // are valid. It also checks that the same key isn't being reset.
   159  func (c *configCommand) parseSetKeys(args []string) error {
   160  	options, err := keyvalues.Parse(args, true)
   161  	if err != nil {
   162  		return errors.Trace(err)
   163  	}
   164  	c.values = make(attributes)
   165  	for k, v := range options {
   166  		if k == config.AgentVersionKey {
   167  			return errors.Errorf(`agent-version must be set via "upgrade-juju"`)
   168  		}
   169  		c.values[k] = v
   170  	}
   171  
   172  	for _, k := range c.resetKeys {
   173  		if _, ok := c.values[k]; ok {
   174  			return errors.Errorf(
   175  				"key %q cannot be both set and reset in the same command", k)
   176  		}
   177  	}
   178  
   179  	c.action = c.setConfig
   180  	return nil
   181  }
   182  
   183  // parseResetKeys splits the keys provided to --reset after trimming any
   184  // leading or trailing comma. It then verifies that we haven't incorrectly
   185  // received any key=value pairs and finally sets the value(s) on c.resetKeys.
   186  func (c *configCommand) parseResetKeys() error {
   187  	if len(c.reset) == 0 {
   188  		return nil
   189  	}
   190  	var resetKeys []string
   191  	for _, value := range c.reset {
   192  		keys := strings.Split(strings.Trim(value, ","), ",")
   193  		resetKeys = append(resetKeys, keys...)
   194  	}
   195  
   196  	for _, k := range resetKeys {
   197  		if k == config.AgentVersionKey {
   198  			return errors.Errorf("%q cannot be reset", config.AgentVersionKey)
   199  		}
   200  		if strings.Contains(k, "=") {
   201  			return errors.Errorf(
   202  				`--reset accepts a comma delimited set of keys "a,b,c", received: %q`, k)
   203  		}
   204  	}
   205  	c.resetKeys = resetKeys
   206  	return nil
   207  }
   208  
   209  // getAPI returns the API. This allows passing in a test configCommandAPI
   210  // implementation.
   211  func (c *configCommand) getAPI() (configCommandAPI, error) {
   212  	if c.api != nil {
   213  		return c.api, nil
   214  	}
   215  	api, err := c.NewAPIRoot()
   216  	if err != nil {
   217  		return nil, errors.Annotate(err, "opening API connection")
   218  	}
   219  	client := modelconfig.NewClient(api)
   220  	return client, nil
   221  }
   222  
   223  // Run implements the meaty part of the cmd.Command interface.
   224  func (c *configCommand) Run(ctx *cmd.Context) error {
   225  	client, err := c.getAPI()
   226  	if err != nil {
   227  		return err
   228  	}
   229  	defer client.Close()
   230  
   231  	if len(c.resetKeys) > 0 {
   232  		err := c.resetConfig(client, ctx)
   233  		if err != nil {
   234  			// We return this error naked as it is almost certainly going to be
   235  			// cmd.ErrSilent and the cmd.Command framework expects that back
   236  			// from cmd.Run if the process is blocked.
   237  			return err
   238  		}
   239  	}
   240  	if c.action == nil {
   241  		// If we are reset only we end up here, only we've already done that.
   242  		return nil
   243  	}
   244  	return c.action(client, ctx)
   245  }
   246  
   247  // reset unsets the keys provided to the command.
   248  func (c *configCommand) resetConfig(client configCommandAPI, ctx *cmd.Context) error {
   249  	// ctx unused in this method
   250  	if err := c.verifyKnownKeys(client); err != nil {
   251  		return errors.Trace(err)
   252  	}
   253  
   254  	return block.ProcessBlockedError(client.ModelUnset(c.resetKeys...), block.BlockChange)
   255  }
   256  
   257  // set sets the provided key/value pairs on the model.
   258  func (c *configCommand) setConfig(client configCommandAPI, ctx *cmd.Context) error {
   259  	// ctx unused in this method.
   260  	envAttrs, err := client.ModelGet()
   261  	if err != nil {
   262  		return err
   263  	}
   264  	for key := range c.values {
   265  		if _, exists := envAttrs[key]; !exists {
   266  			logger.Warningf("key %q is not defined in the current model configuration: possible misspelling", key)
   267  		}
   268  
   269  	}
   270  	return block.ProcessBlockedError(client.ModelSet(c.values), block.BlockChange)
   271  }
   272  
   273  // get writes the value of a single key or the full output for the model to the cmd.Context.
   274  func (c *configCommand) getConfig(client configCommandAPI, ctx *cmd.Context) error {
   275  	attrs, err := client.ModelGetWithMetadata()
   276  	if err != nil {
   277  		return err
   278  	}
   279  
   280  	for attrName := range attrs {
   281  		// We don't want model attributes included, these are available
   282  		// via show-model.
   283  		if c.isModelAttrbute(attrName) {
   284  			delete(attrs, attrName)
   285  		}
   286  	}
   287  
   288  	if len(c.keys) == 1 {
   289  		key := c.keys[0]
   290  		if value, found := attrs[key]; found {
   291  			if c.out.Name() == "tabular" {
   292  				return cmd.FormatYaml(ctx.Stdout, value.Value)
   293  			}
   294  			attrs = config.ConfigValues{
   295  				key: config.ConfigValue{
   296  					Source: value.Source,
   297  					Value:  value.Value,
   298  				},
   299  			}
   300  		} else {
   301  			return errors.Errorf("key %q not found in %q model.", key, attrs["name"])
   302  		}
   303  	}
   304  	return c.out.Write(ctx, attrs)
   305  }
   306  
   307  // verifyKnownKeys is a helper to validate the keys we are operating with
   308  // against the set of known attributes from the model.
   309  func (c *configCommand) verifyKnownKeys(client configCommandAPI) error {
   310  	known, err := client.ModelGet()
   311  	if err != nil {
   312  		return errors.Trace(err)
   313  	}
   314  
   315  	allKeys := c.resetKeys[:]
   316  	for k := range c.values {
   317  		allKeys = append(allKeys, k)
   318  	}
   319  
   320  	for _, key := range allKeys {
   321  		// check if the key exists in the known config
   322  		// and warn the user if the key is not defined
   323  		if _, exists := known[key]; !exists {
   324  			logger.Warningf(
   325  				"key %q is not defined in the current model configuration: possible misspelling", key)
   326  		}
   327  	}
   328  	return nil
   329  }
   330  
   331  // isModelAttribute returns if the supplied attribute is a valid model
   332  // attribute.
   333  func (c *configCommand) isModelAttrbute(attr string) bool {
   334  	switch attr {
   335  	case config.NameKey, config.TypeKey, config.UUIDKey:
   336  		return true
   337  	}
   338  	return false
   339  }
   340  
   341  // formatConfigTabular writes a tabular summary of config information.
   342  func formatConfigTabular(writer io.Writer, value interface{}) error {
   343  	configValues, ok := value.(config.ConfigValues)
   344  	if !ok {
   345  		return errors.Errorf("expected value of type %T, got %T", configValues, value)
   346  	}
   347  
   348  	tw := output.TabWriter(writer)
   349  	w := output.Wrapper{tw}
   350  
   351  	var valueNames []string
   352  	for name := range configValues {
   353  		valueNames = append(valueNames, name)
   354  	}
   355  	sort.Strings(valueNames)
   356  	w.Println("Attribute", "From", "Value")
   357  
   358  	for _, name := range valueNames {
   359  		info := configValues[name]
   360  		out := &bytes.Buffer{}
   361  		err := cmd.FormatYaml(out, info.Value)
   362  		if err != nil {
   363  			return errors.Annotatef(err, "formatting value for %q", name)
   364  		}
   365  		// Some attribute values have a newline appended
   366  		// which makes the output messy.
   367  		valString := strings.TrimSuffix(out.String(), "\n")
   368  		w.Println(name, info.Source, valString)
   369  	}
   370  
   371  	tw.Flush()
   372  	return nil
   373  }