github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/application/config.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  package application
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"strings"
    11  	"unicode/utf8"
    12  
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/gnuflag"
    16  
    17  	"github.com/juju/juju/api/application"
    18  	"github.com/juju/juju/apiserver/params"
    19  	"github.com/juju/juju/cmd/juju/block"
    20  	"github.com/juju/juju/cmd/modelcmd"
    21  	"github.com/juju/juju/cmd/output"
    22  	"github.com/juju/utils/keyvalues"
    23  )
    24  
    25  const maxValueSize = 5242880 // Max size for a config file.
    26  
    27  const (
    28  	configSummary = `Gets, sets, or resets configuration for a deployed application.`
    29  	configDetails = `By default, all configuration (keys, values, metadata) for the application are
    30  displayed if a key is not specified.
    31  
    32  Output includes the name of the charm used to deploy the application and a
    33  listing of the application-specific configuration settings.
    34  See ` + "`juju status`" + ` for application names.
    35  
    36  Examples:
    37      juju config apache2
    38      juju config --format=json apache2
    39      juju config mysql dataset-size
    40      juju config mysql --reset dataset-size,backup_dir
    41      juju config apache2 --file path/to/config.yaml
    42      juju config mysql dataset-size=80% backup_dir=/vol1/mysql/backups
    43      juju config apache2 --model mymodel --file /home/ubuntu/mysql.yaml
    44  
    45  See also:
    46      deploy
    47      status
    48  `
    49  )
    50  
    51  // NewConfigCommand returns a command used to get, reset, and set application
    52  // attributes.
    53  func NewConfigCommand() cmd.Command {
    54  	return modelcmd.Wrap(&configCommand{})
    55  }
    56  
    57  type attributes map[string]string
    58  
    59  // configCommand get, sets, and resets configuration values of an application.
    60  type configCommand struct {
    61  	api configCommandAPI
    62  	modelcmd.ModelCommandBase
    63  	out cmd.Output
    64  
    65  	action          func(configCommandAPI, *cmd.Context) error // get, set, or reset action set in  Init
    66  	applicationName string
    67  	configFile      cmd.FileVar
    68  	keys            []string
    69  	reset           []string // Holds the keys to be reset until parsed.
    70  	resetKeys       []string // Holds the keys to be reset once parsed.
    71  	useFile         bool
    72  	values          attributes
    73  }
    74  
    75  // configCommandAPI is an interface to allow passing in a fake implementation under test.
    76  type configCommandAPI interface {
    77  	Close() error
    78  	Update(args params.ApplicationUpdate) error
    79  	Get(application string) (*params.ApplicationGetResults, error)
    80  	Set(application string, options map[string]string) error
    81  	Unset(application string, options []string) error
    82  }
    83  
    84  // Info is part of the cmd.Command interface.
    85  func (c *configCommand) Info() *cmd.Info {
    86  	return &cmd.Info{
    87  		Name:    "config",
    88  		Args:    "<application name> [--reset <key[,key]>] [<attribute-key>][=<value>] ...]",
    89  		Purpose: configSummary,
    90  		Doc:     configDetails,
    91  	}
    92  }
    93  
    94  // SetFlags is part of the cmd.Command interface.
    95  func (c *configCommand) SetFlags(f *gnuflag.FlagSet) {
    96  	c.ModelCommandBase.SetFlags(f)
    97  	c.out.AddFlags(f, "yaml", output.DefaultFormatters)
    98  	f.Var(&c.configFile, "file", "path to yaml-formatted application config")
    99  	f.Var(cmd.NewAppendStringsValue(&c.reset), "reset", "Reset the provided comma delimited keys")
   100  }
   101  
   102  // getAPI either uses the fake API set at test time or that is nil, gets a real
   103  // API and sets that as the API.
   104  func (c *configCommand) getAPI() (configCommandAPI, error) {
   105  	if c.api != nil {
   106  		return c.api, nil
   107  	}
   108  	root, err := c.NewAPIRoot()
   109  	if err != nil {
   110  		return nil, errors.Trace(err)
   111  	}
   112  	client := application.NewClient(root)
   113  	return client, nil
   114  }
   115  
   116  // Init is part of the cmd.Command interface.
   117  func (c *configCommand) Init(args []string) error {
   118  	if len(args) == 0 || len(strings.Split(args[0], "=")) > 1 {
   119  		return errors.New("no application name specified")
   120  	}
   121  
   122  	// If there are arguments provided to reset, we turn it into a slice of
   123  	// strings and verify them. If there is one or more valid keys to reset and
   124  	// no other errors initalizing the command, c.resetDefaults will be called
   125  	// in c.Run.
   126  	if err := c.parseResetKeys(); err != nil {
   127  		return errors.Trace(err)
   128  	}
   129  
   130  	c.applicationName = args[0]
   131  	args = args[1:]
   132  
   133  	switch len(args) {
   134  	case 0:
   135  		return c.handleZeroArgs()
   136  	case 1:
   137  		return c.handleOneArg(args)
   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 there's a path we're setting args from a file
   146  	if c.configFile.Path != "" {
   147  		return c.parseSet([]string{})
   148  	}
   149  	if len(c.reset) == 0 {
   150  		// If there's nothing to reset we're getting all the settings.
   151  		c.action = c.getConfig
   152  	}
   153  	// Otherwise just reset.
   154  	return nil
   155  }
   156  
   157  // handleOneArg handles the case where there is one positional arg.
   158  func (c *configCommand) handleOneArg(args []string) error {
   159  	// If there's an '=', this must be setting a value
   160  	if strings.Contains(args[0], "=") {
   161  		return c.parseSet(args)
   162  	}
   163  	// If there's no reset,	we want to get a single value
   164  	if len(c.reset) == 0 {
   165  		c.action = c.getConfig
   166  		c.keys = args
   167  		return nil
   168  	}
   169  	// Otherwise we have reset and a get arg, which is invalid.
   170  	return errors.New("cannot reset and retrieve values simultaneously")
   171  }
   172  
   173  // handleArgs handles the case where there's more than one positional arg.
   174  func (c *configCommand) handleArgs(args []string) error {
   175  	// This must be setting values but let's make sure.
   176  	var pairs, numArgs int
   177  	numArgs = len(args)
   178  	for _, a := range args {
   179  		if strings.Contains(a, "=") {
   180  			pairs++
   181  		}
   182  	}
   183  	if pairs == numArgs {
   184  		return c.parseSet(args)
   185  	}
   186  	if pairs == 0 {
   187  		return errors.New("can only retrieve a single value, or all values")
   188  	}
   189  	return errors.New("cannot set and retrieve values simultaneously")
   190  }
   191  
   192  // parseResetKeys splits the keys provided to --reset.
   193  func (c *configCommand) parseResetKeys() error {
   194  	if len(c.reset) == 0 {
   195  		return nil
   196  	}
   197  	var resetKeys []string
   198  	for _, value := range c.reset {
   199  		keys := strings.Split(strings.Trim(value, ","), ",")
   200  		resetKeys = append(resetKeys, keys...)
   201  	}
   202  	for _, k := range resetKeys {
   203  		if strings.Contains(k, "=") {
   204  			return errors.Errorf(
   205  				`--reset accepts a comma delimited set of keys "a,b,c", received: %q`, k)
   206  		}
   207  	}
   208  
   209  	c.resetKeys = resetKeys
   210  	return nil
   211  }
   212  
   213  // parseSet parses the command line args when --file is set or if the
   214  // positional args are key=value pairs.
   215  func (c *configCommand) parseSet(args []string) error {
   216  	file := c.configFile.Path != ""
   217  	if file && len(args) > 0 {
   218  		return errors.New("cannot specify --file and key=value arguments simultaneously")
   219  	}
   220  	c.action = c.setConfig
   221  	if file {
   222  		c.useFile = true
   223  		return nil
   224  	}
   225  
   226  	settings, err := keyvalues.Parse(args, true)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	c.values = settings
   231  
   232  	return nil
   233  }
   234  
   235  // Run implements the cmd.Command interface.
   236  func (c *configCommand) Run(ctx *cmd.Context) error {
   237  	client, err := c.getAPI()
   238  	if err != nil {
   239  		return errors.Trace(err)
   240  	}
   241  	defer client.Close()
   242  	if len(c.resetKeys) > 0 {
   243  		if err := c.resetConfig(client, ctx); err != nil {
   244  			// We return this error naked as it is almost certainly going to be
   245  			// cmd.ErrSilent and the cmd.Command framework expects that back
   246  			// from cmd.Run if the process is blocked.
   247  			return err
   248  		}
   249  	}
   250  	if c.action == nil {
   251  		// If we are reset only we end up here, only we've already done that.
   252  		return nil
   253  	}
   254  
   255  	return c.action(client, ctx)
   256  }
   257  
   258  // resetConfig is the run action when we are resetting attributes.
   259  func (c *configCommand) resetConfig(client configCommandAPI, ctx *cmd.Context) error {
   260  	return block.ProcessBlockedError(client.Unset(c.applicationName, c.resetKeys), block.BlockChange)
   261  }
   262  
   263  // setConfig is the run action when we are setting new attribute values as args
   264  // or as a file passed in.
   265  func (c *configCommand) setConfig(client configCommandAPI, ctx *cmd.Context) error {
   266  	if c.useFile {
   267  		return c.setConfigFromFile(client, ctx)
   268  	}
   269  
   270  	settings, err := c.validateValues(ctx)
   271  	if err != nil {
   272  		return errors.Trace(err)
   273  	}
   274  
   275  	result, err := client.Get(c.applicationName)
   276  	if err != nil {
   277  		return err
   278  	}
   279  
   280  	for k, v := range settings {
   281  		configValue := result.Config[k]
   282  
   283  		configValueMap, ok := configValue.(map[string]interface{})
   284  		if ok {
   285  			// convert the value to string and compare
   286  			if fmt.Sprintf("%v", configValueMap["value"]) == v {
   287  				logger.Warningf("the configuration setting %q already has the value %q", k, v)
   288  			}
   289  		}
   290  	}
   291  
   292  	return block.ProcessBlockedError(client.Set(c.applicationName, settings), block.BlockChange)
   293  }
   294  
   295  // setConfigFromFile sets the application configuration from settings passed
   296  // in a YAML file.
   297  func (c *configCommand) setConfigFromFile(client configCommandAPI, ctx *cmd.Context) error {
   298  	var (
   299  		b   []byte
   300  		err error
   301  	)
   302  	if c.configFile.Path == "-" {
   303  		buf := bytes.Buffer{}
   304  		buf.ReadFrom(ctx.Stdin)
   305  		b = buf.Bytes()
   306  	} else {
   307  		b, err = c.configFile.Read(ctx)
   308  		if err != nil {
   309  			return err
   310  		}
   311  	}
   312  	return block.ProcessBlockedError(
   313  		client.Update(
   314  			params.ApplicationUpdate{
   315  				ApplicationName: c.applicationName,
   316  				SettingsYAML:    string(b)}), block.BlockChange)
   317  }
   318  
   319  // getConfig is the run action to return one or all configuration values.
   320  func (c *configCommand) getConfig(client configCommandAPI, ctx *cmd.Context) error {
   321  	results, err := client.Get(c.applicationName)
   322  	if err != nil {
   323  		return err
   324  	}
   325  	if len(c.keys) == 1 {
   326  		key := c.keys[0]
   327  		info, found := results.Config[key].(map[string]interface{})
   328  		if !found {
   329  			return errors.Errorf("key %q not found in %q application settings.", key, c.applicationName)
   330  		}
   331  		out := &bytes.Buffer{}
   332  		err := cmd.FormatYaml(out, info["value"])
   333  		if err != nil {
   334  			return err
   335  		}
   336  		fmt.Fprint(ctx.Stdout, out.String())
   337  		return nil
   338  	}
   339  
   340  	resultsMap := map[string]interface{}{
   341  		"application": results.Application,
   342  		"charm":       results.Charm,
   343  		"settings":    results.Config,
   344  	}
   345  	return c.out.Write(ctx, resultsMap)
   346  }
   347  
   348  // validateValues reads the values provided as args and validates that they are
   349  // valid UTF-8.
   350  func (c *configCommand) validateValues(ctx *cmd.Context) (map[string]string, error) {
   351  	settings := map[string]string{}
   352  	for k, v := range c.values {
   353  		//empty string is also valid as a setting value
   354  		if v == "" {
   355  			settings[k] = v
   356  			continue
   357  		}
   358  
   359  		if v[0] != '@' {
   360  			if !utf8.ValidString(v) {
   361  				return nil, errors.Errorf("value for option %q contains non-UTF-8 sequences", k)
   362  			}
   363  			settings[k] = v
   364  			continue
   365  		}
   366  		nv, err := readValue(ctx, v[1:])
   367  		if err != nil {
   368  			return nil, errors.Trace(err)
   369  		}
   370  		if !utf8.ValidString(nv) {
   371  			return nil, errors.Errorf("value for option %q contains non-UTF-8 sequences", k)
   372  		}
   373  		settings[k] = nv
   374  	}
   375  	return settings, nil
   376  }
   377  
   378  // readValue reads the value of an option out of the named file.
   379  // An empty content is valid, like in parsing the options. The upper
   380  // size is 5M.
   381  func readValue(ctx *cmd.Context, filename string) (string, error) {
   382  	absFilename := ctx.AbsPath(filename)
   383  	fi, err := os.Stat(absFilename)
   384  	if err != nil {
   385  		return "", errors.Errorf("cannot read option from file %q: %v", filename, err)
   386  	}
   387  	if fi.Size() > maxValueSize {
   388  		return "", errors.Errorf("size of option file is larger than 5M")
   389  	}
   390  	content, err := ioutil.ReadFile(ctx.AbsPath(filename))
   391  	if err != nil {
   392  		return "", errors.Errorf("cannot read option from file %q: %v", filename, err)
   393  	}
   394  	return string(content), nil
   395  }