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 }