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 }