github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/model/defaultscommand.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/names.v2" 17 18 "github.com/juju/juju/api" 19 "github.com/juju/juju/api/base" 20 cloudapi "github.com/juju/juju/api/cloud" 21 "github.com/juju/juju/api/modelmanager" 22 jujucloud "github.com/juju/juju/cloud" 23 jujucmd "github.com/juju/juju/cmd" 24 "github.com/juju/juju/cmd/juju/block" 25 "github.com/juju/juju/cmd/juju/common" 26 "github.com/juju/juju/cmd/modelcmd" 27 "github.com/juju/juju/cmd/output" 28 "github.com/juju/juju/environs/config" 29 ) 30 31 const ( 32 modelDefaultsSummary = `Displays or sets default configuration settings for a model.` 33 modelDefaultsHelpDoc = ` 34 By default, all default configuration (keys and values) are 35 displayed if a key is not specified. Supplying key=value will set the 36 supplied key to the supplied value. This can be repeated for multiple keys. 37 You can also specify a yaml file containing key values. 38 By default, the model is the current model. 39 40 41 Examples: 42 juju model-defaults 43 juju model-defaults http-proxy 44 juju model-defaults aws/us-east-1 http-proxy 45 juju model-defaults us-east-1 http-proxy 46 juju model-defaults -m mymodel type 47 juju model-defaults ftp-proxy=10.0.0.1:8000 48 juju model-defaults aws/us-east-1 ftp-proxy=10.0.0.1:8000 49 juju model-defaults us-east-1 ftp-proxy=10.0.0.1:8000 50 juju model-defaults us-east-1 ftp-proxy=10.0.0.1:8000 path/to/file.yaml 51 juju model-defaults us-east-1 path/to/file.yaml 52 juju model-defaults -m othercontroller:mymodel default-series=yakkety test-mode=false 53 juju model-defaults --reset default-series test-mode 54 juju model-defaults aws/us-east-1 --reset http-proxy 55 juju model-defaults us-east-1 --reset http-proxy 56 57 See also: 58 models 59 model-config 60 ` 61 ) 62 63 // NewDefaultsCommand wraps defaultsCommand with sane model settings. 64 func NewDefaultsCommand() cmd.Command { 65 defaultsCmd := &defaultsCommand{ 66 newCloudAPI: func(caller base.APICallCloser) cloudAPI { 67 return cloudapi.NewClient(caller) 68 }, 69 newDefaultsAPI: func(caller base.APICallCloser) defaultsCommandAPI { 70 return modelmanager.NewClient(caller) 71 }, 72 } 73 defaultsCmd.newAPIRoot = defaultsCmd.NewAPIRoot 74 return modelcmd.WrapController(defaultsCmd) 75 } 76 77 // defaultsCommand is compound command for accessing and setting attributes 78 // related to default model configuration. 79 type defaultsCommand struct { 80 out cmd.Output 81 modelcmd.ControllerCommandBase 82 83 newAPIRoot func() (api.Connection, error) 84 newDefaultsAPI func(base.APICallCloser) defaultsCommandAPI 85 newCloudAPI func(base.APICallCloser) cloudAPI 86 87 // args holds all the command-line arguments before 88 // they've been parsed. 89 args []string 90 91 action func(defaultsCommandAPI, *cmd.Context) error // The function handling the input, set in Init. 92 key string 93 resetKeys []string // Holds the keys to be reset once parsed. 94 cloudName, regionName string 95 reset []string // Holds the keys to be reset until parsed. 96 setOptions common.ConfigFlag 97 } 98 99 // cloudAPI defines an API to be passed in for testing. 100 type cloudAPI interface { 101 Close() error 102 DefaultCloud() (names.CloudTag, error) 103 Cloud(names.CloudTag) (jujucloud.Cloud, error) 104 } 105 106 // defaultsCommandAPI defines an API to be used during testing. 107 type defaultsCommandAPI interface { 108 // Close closes the api connection. 109 Close() error 110 111 // ModelDefaults returns the default config values used when creating a new model. 112 ModelDefaults() (config.ModelDefaultAttributes, error) 113 114 // SetModelDefaults sets the default config values to use 115 // when creating new models. 116 SetModelDefaults(cloud, region string, config map[string]interface{}) error 117 118 // UnsetModelDefaults clears the default model 119 // configuration values. 120 UnsetModelDefaults(cloud, region string, keys ...string) error 121 } 122 123 // Info implements part of the cmd.Command interface. 124 func (c *defaultsCommand) Info() *cmd.Info { 125 return jujucmd.Info(&cmd.Info{ 126 Args: "[[<cloud/>]<region> ]<model-key>[<=value>] ...]", 127 Doc: modelDefaultsHelpDoc, 128 Name: "model-defaults", 129 Purpose: modelDefaultsSummary, 130 Aliases: []string{"model-default"}, 131 }) 132 } 133 134 // SetFlags implements part of the cmd.Command interface. 135 func (c *defaultsCommand) SetFlags(f *gnuflag.FlagSet) { 136 c.ControllerCommandBase.SetFlags(f) 137 138 c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{ 139 "yaml": cmd.FormatYaml, 140 "json": cmd.FormatJson, 141 "tabular": formatDefaultConfigTabular, 142 }) 143 f.Var(cmd.NewAppendStringsValue(&c.reset), "reset", "Reset the provided comma delimited keys") 144 } 145 146 // Init implements cmd.Command.Init. 147 func (c *defaultsCommand) Init(args []string) error { 148 // There's no way of distinguishing a cloud name 149 // from a model configuration setting without contacting the 150 // API, but we aren't allowed to contact the API at Init time, 151 // so we defer parsing the arguments until Run is called. 152 c.args = args 153 return nil 154 } 155 156 // Run implements part of the cmd.Command interface. 157 func (c *defaultsCommand) Run(ctx *cmd.Context) error { 158 if err := c.parseArgs(c.args); err != nil { 159 return errors.Trace(err) 160 } 161 root, err := c.newAPIRoot() 162 if err != nil { 163 return errors.Trace(err) 164 } 165 client := c.newDefaultsAPI(root) 166 if err != nil { 167 return errors.Trace(err) 168 } 169 defer client.Close() 170 171 if len(c.resetKeys) > 0 { 172 err := c.resetDefaults(client, ctx) 173 if err != nil { 174 // We return this error naked as it is almost certainly going to be 175 // cmd.ErrSilent and the cmd.Command framework expects that back 176 // from cmd.Run if the process is blocked. 177 return err 178 } 179 } 180 if c.action == nil { 181 // If we are reset only we end up here, only we've already done that. 182 return nil 183 } 184 return c.action(client, ctx) 185 } 186 187 // This needs to parse a command line invocation to reset and set, or get 188 // model-default values. The arguments may be interspersed as demonstrated in 189 // the examples. 190 // 191 // This sets foo=baz and unsets bar in aws/us-east-1 192 // juju model-defaults aws/us-east-1 foo=baz --reset bar 193 // 194 // If aws is the cloud of the current or specified controller -- specified by 195 // -c somecontroller -- then the following would also be equivalent. 196 // juju model-defaults --reset bar us-east-1 foo=baz 197 // 198 // If one doesn't specify a cloud or region the command is still valid but for 199 // setting the default on the controller: 200 // juju model-defaults foo=baz --reset bar 201 // 202 // Of course one can specify multiple keys to reset --reset a,b,c and one can 203 // also specify multiple values to set a=b c=d e=f. I.e. comma separated for 204 // resetting and space separated for setting. One may also only set or reset as 205 // a singular action. 206 // juju model-defaults --reset foo 207 // juju model-defaults a=b c=d e=f 208 // juju model-defaults a=b c=d --reset e,f 209 // 210 // cloud/region may also be specified so above examples with that option might 211 // be like the following invokation. 212 // juju model-defaults us-east-1 a=b c=d --reset e,f 213 // 214 // Finally one can also ask for the all the defaults or the defaults for one 215 // specific setting. In this case specifying a region is not valid as 216 // model-defaults shows the settings for a value at all locations that it has a 217 // default set -- or at a minimum the default and "-" for a controller with no 218 // value set. 219 // juju model-defaults 220 // juju model-defaults no-proxy 221 // 222 // It is not valid to reset and get or to set and get values. It is also 223 // neither valid to reset and set the same key, nor to set the same key to 224 // different values in the same command. 225 // 226 // For those playing along that all means the first positional arg can be a 227 // cloud/region, a region, a key=value to set, a key to get the settings for, 228 // or empty. Other caveats are that one cannot set and reset a value for the 229 // same key, that is to say keys to be mutated must be unique. 230 // 231 // Here we go... 232 func (c *defaultsCommand) parseArgs(args []string) error { 233 var err error 234 // If there's nothing to reset and no args we're returning everything. So 235 // we short circuit immediately. 236 if len(args) == 0 && len(c.reset) == 0 { 237 c.action = c.getDefaults 238 return nil 239 } 240 241 // If there is an argument provided to reset, we turn it into a slice of 242 // strings and verify them. If there is one or more valid keys to reset and 243 // no other errors initializing the command, c.resetDefaults will be called 244 // in c.Run. 245 if err = c.parseResetKeys(); err != nil { 246 return errors.Trace(err) 247 } 248 249 // Look at the first positional arg and test to see if it is a valid 250 // optional specification of cloud/region or region. If it is then 251 // cloudName and regionName are set on the object and the positional args 252 // are returned without the first element. If it cannot be validated; 253 // cloudName and regionName are left empty and we get back the same args we 254 // passed in. 255 args, err = c.parseArgsForRegion(args) 256 if err != nil { 257 return errors.Trace(err) 258 } 259 260 // Remember we *might* have one less arg at this point if we chopped the 261 // first off because it was a valid cloud/region option. 262 wantSet := false 263 if len(args) > 0 { 264 lastArg := args[len(args)-1] 265 // We may have a config.yaml file 266 _, err := os.Stat(lastArg) 267 wantSet = err == nil || strings.Contains(lastArg, "=") 268 } 269 270 switch { 271 case wantSet: 272 // In the event that we are setting values, the final positional arg 273 // will always have an "=" in it. So if we see that we know we want to 274 // set args. 275 return c.handleSetArgs(args) 276 case len(args) == 0: 277 if len(c.resetKeys) == 0 { 278 // If there's no positional args and reset is not set then we're 279 // getting all attrs. 280 c.action = c.getDefaults 281 return nil 282 } 283 // Reset only. 284 return nil 285 case len(args) == 1: 286 // We want to get settings for the provided key. 287 return c.handleOneArg(args[0]) 288 default: // case args > 1 289 // Specifying any non key=value positional args after a key=value pair 290 // is invalid input. So if we have more than one the input is almost 291 // certainly invalid, but in different possible ways. 292 return c.handleExtraArgs(args) 293 } 294 } 295 296 // parseResetKeys splits the keys provided to --reset after trimming any 297 // leading or trailing comma. It then verifies that we haven't incorrectly 298 // received any key=value pairs and finally sets the value(s) on c.resetKeys. 299 func (c *defaultsCommand) parseResetKeys() error { 300 if len(c.reset) == 0 { 301 return nil 302 } 303 var resetKeys []string 304 for _, value := range c.reset { 305 keys := strings.Split(strings.Trim(value, ","), ",") 306 resetKeys = append(resetKeys, keys...) 307 } 308 309 for _, k := range resetKeys { 310 if k == config.AgentVersionKey { 311 return errors.Errorf("%q cannot be reset", config.AgentVersionKey) 312 } 313 if strings.Contains(k, "=") { 314 return errors.Errorf( 315 `--reset accepts a comma delimited set of keys "a,b,c", received: %q`, k) 316 } 317 } 318 c.resetKeys = resetKeys 319 return nil 320 } 321 322 // parseArgsForRegion parses args to check if the first arg is a region and 323 // returns the appropriate remaining args. 324 func (c *defaultsCommand) parseArgsForRegion(args []string) ([]string, error) { 325 var err error 326 if len(args) > 0 { 327 // determine if the first arg is cloud/region or region and return 328 // appropriate positional args. 329 args, err = c.parseCloudRegion(args) 330 if err != nil { 331 return nil, errors.Trace(err) 332 } 333 } 334 return args, nil 335 } 336 337 // parseCloudRegion examines args to see if the first arg is a cloud/region or 338 // region. If not it returns the full args slice. If it is then it sets cloud 339 // and/or region on the object and sends the remaining args back to the caller. 340 func (c *defaultsCommand) parseCloudRegion(args []string) ([]string, error) { 341 var cloud, region string 342 cr := args[0] 343 // Must have no more than one slash and it must not be at the beginning or end. 344 if strings.Count(cr, "/") == 1 && !strings.HasPrefix(cr, "/") && !strings.HasSuffix(cr, "/") { 345 elems := strings.Split(cr, "/") 346 cloud, region = elems[0], elems[1] 347 } else { 348 region = cr 349 } 350 351 // TODO(redir) 2016-10-05 #1627162 352 // We don't disallow "=" in region names, but probably should. 353 if strings.Contains(region, "=") { 354 return args, nil 355 } 356 357 valid, err := c.validCloudRegion(cloud, region) 358 if err != nil { 359 return nil, errors.Trace(err) 360 } 361 if !valid { 362 return args, nil 363 } 364 return args[1:], nil 365 } 366 367 // validCloudRegion checks that region is a valid region in cloud, or default cloud 368 // if cloud is not specified. 369 func (c *defaultsCommand) validCloudRegion(cloudName, region string) (bool, error) { 370 var ( 371 isCloudRegion bool 372 cloud jujucloud.Cloud 373 cTag names.CloudTag 374 err error 375 ) 376 377 root, err := c.newAPIRoot() 378 if err != nil { 379 return false, errors.Trace(err) 380 } 381 cc := c.newCloudAPI(root) 382 defer cc.Close() 383 384 if cloudName == "" { 385 cTag, err = cc.DefaultCloud() 386 if err != nil { 387 return false, errors.Trace(err) 388 } 389 } else { 390 if !names.IsValidCloud(cloudName) { 391 return false, errors.Errorf("invalid cloud %q", cloudName) 392 } 393 cTag = names.NewCloudTag(cloudName) 394 } 395 cloud, err = cc.Cloud(cTag) 396 if err != nil { 397 return false, errors.Trace(err) 398 } 399 400 for _, r := range cloud.Regions { 401 if r.Name == region { 402 c.cloudName = cTag.Id() 403 c.regionName = region 404 isCloudRegion = true 405 break 406 } 407 } 408 return isCloudRegion, nil 409 } 410 411 // handleSetArgs parses args for setting defaults. 412 func (c *defaultsCommand) handleSetArgs(args []string) error { 413 // We may have a config.yaml file 414 _, err := os.Stat(args[0]) 415 argZeroKeyOnly := err != nil && !strings.Contains(args[0], "=") 416 // If an invalid region was specified, the first positional arg won't have 417 // an "=". If we see one here we know it is invalid. 418 switch { 419 case argZeroKeyOnly && c.regionName == "": 420 return errors.Errorf("invalid region specified: %q", args[0]) 421 case argZeroKeyOnly && c.regionName != "": 422 return errors.New("cannot set and retrieve default values simultaneously") 423 default: 424 if err := c.parseSetKeys(args); err != nil { 425 return errors.Trace(err) 426 } 427 c.action = c.setDefaults 428 return nil 429 } 430 } 431 432 // parseSetKeys iterates over the args and make sure that the key=value pairs 433 // are valid. It also checks that the same key isn't also being reset. 434 func (c *defaultsCommand) parseSetKeys(args []string) error { 435 for _, arg := range args { 436 if err := c.setOptions.Set(arg); err != nil { 437 return errors.Trace(err) 438 } 439 } 440 return nil 441 } 442 443 // handleOneArg handles the case where we have one positional arg after 444 // processing for a region and the reset flag. 445 func (c *defaultsCommand) handleOneArg(arg string) error { 446 resetSpecified := c.resetKeys != nil 447 regionSpecified := c.regionName != "" 448 449 if regionSpecified { 450 if resetSpecified { 451 // If a region was specified and reset was specified, we shouldn't have 452 // an extra arg. If it had an "=" in it, we should have handled it 453 // already. 454 return errors.New("cannot retrieve defaults for a region and reset attributes at the same time") 455 } 456 } 457 if resetSpecified { 458 // It makes no sense to supply a positional arg that isn't a region if 459 // we are resetting keys in a region, so we must have gotten an invalid 460 // region. 461 return errors.Errorf("invalid region specified: %q", arg) 462 } 463 // We can retrieve a value. 464 c.key = arg 465 c.action = c.getDefaults 466 return nil 467 } 468 469 // handleExtraArgs handles the case where too many args were supplied. 470 func (c *defaultsCommand) handleExtraArgs(args []string) error { 471 resetSpecified := c.resetKeys != nil 472 regionSpecified := c.regionName != "" 473 numArgs := len(args) 474 475 // if we have a key=value pair here then something is wrong because the 476 // last positional arg is not one. We assume the user intended to get a 477 // value after setting them. 478 for _, arg := range args { 479 // We may have a config.yaml file 480 _, err := os.Stat(arg) 481 if err == nil || strings.Contains(arg, "=") { 482 return errors.New("cannot set and retrieve default values simultaneously") 483 } 484 } 485 486 if !regionSpecified { 487 if resetSpecified { 488 if numArgs == 2 { 489 // It makes no sense to supply a positional arg that isn't a 490 // region if we are resetting a region, so we must have gotten 491 // an invalid region. 492 return errors.Errorf("invalid region specified: %q", args[0]) 493 } 494 } 495 if !resetSpecified { 496 // If we land here it is because there are extraneous positional 497 // args. 498 return errors.New("can only retrieve defaults for one key or all") 499 } 500 } 501 return errors.New("invalid input") 502 } 503 504 // getDefaults writes out the value for a single key or the full tree of 505 // defaults. 506 func (c *defaultsCommand) getDefaults(client defaultsCommandAPI, ctx *cmd.Context) error { 507 attrs, err := client.ModelDefaults() 508 if err != nil { 509 return err 510 } 511 512 valueForRegion := func(region string, regions []config.RegionDefaultValue) (config.RegionDefaultValue, bool) { 513 for _, r := range regions { 514 if r.Name == region { 515 return r, true 516 } 517 } 518 return config.RegionDefaultValue{}, false 519 } 520 521 // Filter by region if necessary. 522 if c.regionName != "" { 523 for attrName, attr := range attrs { 524 if regionDefault, ok := valueForRegion(c.regionName, attr.Regions); !ok { 525 delete(attrs, attrName) 526 } else { 527 attrForRegion := attr 528 attrForRegion.Regions = []config.RegionDefaultValue{regionDefault} 529 attrs[attrName] = attrForRegion 530 } 531 } 532 } 533 534 if c.key != "" { 535 if value, ok := attrs[c.key]; ok { 536 attrs = config.ModelDefaultAttributes{ 537 c.key: value, 538 } 539 } else { 540 msg := fmt.Sprintf("there are no default model values for %q", c.key) 541 if c.regionName != "" { 542 msg += fmt.Sprintf(" in region %q", c.regionName) 543 } 544 return errors.New(msg) 545 } 546 } 547 if c.regionName != "" && len(attrs) == 0 { 548 return errors.New(fmt.Sprintf( 549 "there are no default model values in region %q", c.regionName)) 550 } 551 552 // If c.keys is empty, write out the whole lot. 553 return c.out.Write(ctx, attrs) 554 } 555 556 // setDefaults sets defaults as provided in c.values. 557 func (c *defaultsCommand) setDefaults(client defaultsCommandAPI, ctx *cmd.Context) error { 558 attrs, err := c.setOptions.ReadAttrs(ctx) 559 if err != nil { 560 return errors.Trace(err) 561 } 562 var keys []string 563 values := make(attributes) 564 for k, v := range attrs { 565 if k == config.AgentVersionKey { 566 return errors.Errorf(`"agent-version" must be set via "upgrade-model"`) 567 } 568 values[k] = v 569 keys = append(keys, k) 570 } 571 572 for _, k := range c.resetKeys { 573 if _, ok := values[k]; ok { 574 return errors.Errorf( 575 "key %q cannot be both set and unset in the same command", k) 576 } 577 } 578 579 if err := c.verifyKnownKeys(client, keys); err != nil { 580 return errors.Trace(err) 581 } 582 return block.ProcessBlockedError( 583 client.SetModelDefaults( 584 c.cloudName, c.regionName, values), block.BlockChange) 585 } 586 587 // resetDefaults resets the keys in resetKeys. 588 func (c *defaultsCommand) resetDefaults(client defaultsCommandAPI, ctx *cmd.Context) error { 589 // ctx unused in this method. 590 if err := c.verifyKnownKeys(client, c.resetKeys); err != nil { 591 return errors.Trace(err) 592 } 593 return block.ProcessBlockedError( 594 client.UnsetModelDefaults( 595 c.cloudName, c.regionName, c.resetKeys...), block.BlockChange) 596 597 } 598 599 // verifyKnownKeys is a helper to validate the keys we are operating with 600 // against the set of known attributes from the model. 601 func (c *defaultsCommand) verifyKnownKeys(client defaultsCommandAPI, keys []string) error { 602 known, err := client.ModelDefaults() 603 if err != nil { 604 return errors.Trace(err) 605 } 606 607 allKeys := c.resetKeys[:] 608 for _, k := range keys { 609 allKeys = append(allKeys, k) 610 } 611 612 for _, key := range allKeys { 613 // check if the key exists in the known config 614 // and warn the user if the key is not defined 615 if _, exists := known[key]; !exists { 616 logger.Warningf( 617 "key %q is not defined in the known model configuration: possible misspelling", key) 618 } 619 } 620 return nil 621 } 622 623 // formatConfigTabular writes a tabular summary of default config information. 624 func formatDefaultConfigTabular(writer io.Writer, value interface{}) error { 625 defaultValues, ok := value.(config.ModelDefaultAttributes) 626 if !ok { 627 return errors.Errorf("expected value of type %T, got %T", defaultValues, value) 628 } 629 630 tw := output.TabWriter(writer) 631 w := output.Wrapper{tw} 632 633 p := func(name string, value config.AttributeDefaultValues) { 634 var c, d interface{} 635 switch value.Default { 636 case nil: 637 d = "-" 638 case "": 639 d = `""` 640 default: 641 d = value.Default 642 } 643 switch value.Controller { 644 case nil: 645 c = "-" 646 case "": 647 c = `""` 648 default: 649 c = value.Controller 650 } 651 w.Println(name, d, c) 652 for _, region := range value.Regions { 653 w.Println(" "+region.Name, region.Value, "-") 654 } 655 } 656 var valueNames []string 657 for name := range defaultValues { 658 valueNames = append(valueNames, name) 659 } 660 sort.Strings(valueNames) 661 662 w.Println("Attribute", "Default", "Controller") 663 664 for _, name := range valueNames { 665 info := defaultValues[name] 666 out := &bytes.Buffer{} 667 err := cmd.FormatYaml(out, info) 668 if err != nil { 669 return errors.Annotatef(err, "formatting value for %q", name) 670 } 671 p(name, info) 672 } 673 674 tw.Flush() 675 return nil 676 }