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