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 }