github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/application/config_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 package application_test 4 5 import ( 6 "bytes" 7 "io/ioutil" 8 "os" 9 "strings" 10 "unicode/utf8" 11 12 "github.com/juju/cmd" 13 "github.com/juju/cmd/cmdtesting" 14 "github.com/juju/errors" 15 jc "github.com/juju/testing/checkers" 16 "github.com/juju/utils" 17 gc "gopkg.in/check.v1" 18 goyaml "gopkg.in/yaml.v2" 19 20 "github.com/juju/juju/apiserver/common" 21 "github.com/juju/juju/cmd/juju/application" 22 "github.com/juju/juju/core/model" 23 "github.com/juju/juju/feature" 24 "github.com/juju/juju/jujuclient" 25 "github.com/juju/juju/jujuclient/jujuclienttesting" 26 coretesting "github.com/juju/juju/testing" 27 ) 28 29 type configCommandSuite struct { 30 coretesting.FakeJujuXDGDataHomeSuite 31 dir string 32 fake *fakeApplicationAPI 33 store jujuclient.ClientStore 34 defaultCharmValues map[string]interface{} 35 defaultAppValues map[string]interface{} 36 } 37 38 var ( 39 _ = gc.Suite(&configCommandSuite{}) 40 41 validSetTestValue = "a value with spaces\nand newline\nand UTF-8 characters: \U0001F604 / \U0001F44D" 42 invalidSetTestValue = "a value with an invalid UTF-8 sequence: " + string([]byte{0xFF, 0xFF}) 43 yamlConfigValue = "dummy-application:\n skill-level: 9000\n username: admin001\n\n" 44 ) 45 46 var charmSettings = map[string]interface{}{ 47 "multiline-value": map[string]interface{}{ 48 "description": "Specifies multiline-value", 49 "type": "string", 50 "value": "The quick brown fox jumps over the lazy dog. \"The quick brown fox jumps over the lazy dog\" \"The quick brown fox jumps over the lazy dog\" ", 51 }, 52 "title": map[string]interface{}{ 53 "description": "Specifies title", 54 "type": "string", 55 "value": "Nearly There", 56 }, 57 "skill-level": map[string]interface{}{ 58 "description": "Specifies skill-level", 59 "value": 100, 60 "type": "int", 61 }, 62 "username": map[string]interface{}{ 63 "description": "Specifies username", 64 "type": "string", 65 "value": "admin001", 66 }, 67 "outlook": map[string]interface{}{ 68 "description": "Specifies outlook", 69 "type": "string", 70 "value": "true", 71 }, 72 } 73 74 var getTests = []struct { 75 application string 76 useAppConfig bool 77 expected map[string]interface{} 78 }{ 79 { 80 "dummy-application", 81 true, 82 map[string]interface{}{ 83 "application": "dummy-application", 84 "charm": "dummy", 85 "application-config": map[string]interface{}{ 86 "juju-external-hostname": map[string]interface{}{ 87 "description": "Specifies juju-external-hostname", 88 "type": "string", 89 "value": "ext-host", 90 }, 91 }, 92 "settings": charmSettings, 93 "changes will be targeted to generation": interface{}("current"), 94 }, 95 }, { 96 "dummy-application", 97 false, 98 map[string]interface{}{ 99 "application": "dummy-application", 100 "charm": "dummy", 101 "settings": charmSettings, 102 "changes will be targeted to generation": interface{}("current"), 103 }, 104 }, 105 } 106 107 func (s *configCommandSuite) SetUpTest(c *gc.C) { 108 s.FakeJujuXDGDataHomeSuite.SetUpTest(c) 109 s.SetFeatureFlags(feature.Generations) 110 111 s.defaultCharmValues = map[string]interface{}{ 112 "title": "Nearly There", 113 "skill-level": 100, 114 "username": "admin001", 115 "outlook": "true", 116 "multiline-value": "The quick brown fox jumps over the lazy dog. \"The quick brown fox jumps over the lazy dog\" \"The quick brown fox jumps over the lazy dog\" ", 117 } 118 s.defaultAppValues = map[string]interface{}{ 119 "juju-external-hostname": "ext-host", 120 } 121 122 s.fake = &fakeApplicationAPI{ 123 generation: model.GenerationCurrent, 124 name: "dummy-application", 125 charmName: "dummy", 126 charmValues: s.defaultCharmValues, 127 appValues: s.defaultAppValues, 128 version: 6, 129 } 130 131 s.store = jujuclienttesting.MinimalStore() 132 133 s.dir = c.MkDir() 134 c.Assert(utf8.ValidString(validSetTestValue), jc.IsTrue) 135 c.Assert(utf8.ValidString(invalidSetTestValue), jc.IsFalse) 136 setupValueFile(c, s.dir, "valid.txt", validSetTestValue) 137 setupValueFile(c, s.dir, "invalid.txt", invalidSetTestValue) 138 setupBigFile(c, s.dir) 139 setupConfigFile(c, s.dir) 140 } 141 142 func (s *configCommandSuite) TestGetCommandInit(c *gc.C) { 143 // missing args 144 err := cmdtesting.InitCommand(application.NewConfigCommandForTest(s.fake, s.store), []string{}) 145 c.Assert(err, gc.ErrorMatches, "no application name specified") 146 } 147 148 func (s *configCommandSuite) TestGetCommandInitWithApplication(c *gc.C) { 149 err := cmdtesting.InitCommand(application.NewConfigCommandForTest(s.fake, s.store), []string{"app"}) 150 c.Assert(err, jc.ErrorIsNil) 151 } 152 153 func (s *configCommandSuite) TestGetCommandInitWithKey(c *gc.C) { 154 err := cmdtesting.InitCommand(application.NewConfigCommandForTest(s.fake, s.store), []string{"app", "key"}) 155 c.Assert(err, jc.ErrorIsNil) 156 } 157 158 func (s *configCommandSuite) TestGetCommandInitWithGeneration(c *gc.C) { 159 err := cmdtesting.InitCommand( 160 application.NewConfigCommandForTest(s.fake, s.store), []string{"app", "key", "--generation", "current"}) 161 c.Assert(err, jc.ErrorIsNil) 162 } 163 164 func (s *configCommandSuite) TestGetConfig(c *gc.C) { 165 s.SetFeatureFlags(feature.Generations) 166 for _, t := range getTests { 167 if !t.useAppConfig { 168 s.fake.appValues = nil 169 } 170 ctx := cmdtesting.Context(c) 171 code := cmd.Main(application.NewConfigCommandForTest(s.fake, s.store), ctx, []string{t.application}) 172 c.Check(code, gc.Equals, 0) 173 c.Assert(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "") 174 // round trip via goyaml to avoid being sucked into a quagmire of 175 // map[interface{}]interface{} vs map[string]interface{}. This is 176 // also required if we add json support to this command. 177 buf, err := goyaml.Marshal(t.expected) 178 c.Assert(err, jc.ErrorIsNil) 179 expected := make(map[string]interface{}) 180 err = goyaml.Unmarshal(buf, &expected) 181 c.Assert(err, jc.ErrorIsNil) 182 183 actual := make(map[string]interface{}) 184 err = goyaml.Unmarshal(ctx.Stdout.(*bytes.Buffer).Bytes(), &actual) 185 c.Assert(err, jc.ErrorIsNil) 186 c.Assert(actual, jc.DeepEquals, expected) 187 } 188 } 189 190 func (s *configCommandSuite) TestGetCharmConfigKey(c *gc.C) { 191 ctx := cmdtesting.Context(c) 192 code := cmd.Main(application.NewConfigCommandForTest(s.fake, s.store), ctx, []string{"dummy-application", "title"}) 193 c.Check(code, gc.Equals, 0) 194 c.Assert(cmdtesting.Stderr(ctx), gc.Equals, "") 195 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, "Nearly There") 196 } 197 198 func (s *configCommandSuite) TestGetCharmConfigKeyMultilineValue(c *gc.C) { 199 ctx := cmdtesting.Context(c) 200 code := cmd.Main(application.NewConfigCommandForTest(s.fake, s.store), ctx, []string{"dummy-application", "multiline-value"}) 201 c.Check(code, gc.Equals, 0) 202 c.Assert(cmdtesting.Stderr(ctx), gc.Equals, "") 203 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, "The quick brown fox jumps over the lazy dog. \"The quick brown fox jumps over the lazy dog\" \"The quick brown fox jumps over the lazy dog\" ") 204 } 205 206 func (s *configCommandSuite) TestGetCharmConfigKeyMultilineValueJSON(c *gc.C) { 207 ctx := cmdtesting.Context(c) 208 code := cmd.Main(application.NewConfigCommandForTest(s.fake, s.store), ctx, []string{"dummy-application", "multiline-value", "--format", "json"}) 209 c.Check(code, gc.Equals, 0) 210 c.Assert(cmdtesting.Stderr(ctx), gc.Equals, "") 211 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, "The quick brown fox jumps over the lazy dog. \"The quick brown fox jumps over the lazy dog\" \"The quick brown fox jumps over the lazy dog\" ") 212 } 213 214 func (s *configCommandSuite) TestGetAppConfigKey(c *gc.C) { 215 ctx := cmdtesting.Context(c) 216 code := cmd.Main(application.NewConfigCommandForTest( 217 s.fake, s.store), ctx, []string{"dummy-application", "juju-external-hostname"}) 218 c.Check(code, gc.Equals, 0) 219 c.Assert(cmdtesting.Stderr(ctx), gc.Equals, "") 220 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, "ext-host") 221 } 222 223 func (s *configCommandSuite) TestGetConfigKeyNotFound(c *gc.C) { 224 _, err := cmdtesting.RunCommand(c, application.NewConfigCommandForTest(s.fake, s.store), "dummy-application", "invalid") 225 c.Assert(err, gc.ErrorMatches, `key "invalid" not found in "dummy-application" application config or charm settings.`, gc.Commentf("details: %v", errors.Details(err))) 226 } 227 228 var setCommandInitErrorTests = []struct { 229 about string 230 args []string 231 expectError string 232 }{{ 233 about: "no arguments", 234 expectError: "no application name specified", 235 }, { 236 about: "missing application name", 237 args: []string{"name=foo"}, 238 expectError: "no application name specified", 239 }, { 240 about: "--file path, but no application", 241 args: []string{"--file", "testconfig.yaml"}, 242 expectError: "no application name specified", 243 }, { 244 about: "--file and options specified", 245 args: []string{"application", "--file", "testconfig.yaml", "bees="}, 246 expectError: "cannot specify --file and key=value arguments simultaneously", 247 }, { 248 about: "--reset and no config name provided", 249 args: []string{"application", "--reset"}, 250 expectError: "option needs an argument: --reset", 251 }, { 252 about: "cannot set and retrieve simultaneously", 253 args: []string{"application", "get", "set=value"}, 254 expectError: "cannot set and retrieve values simultaneously", 255 }, { 256 about: "cannot reset and get simultaneously", 257 args: []string{"application", "--reset", "reset", "get"}, 258 expectError: "cannot reset and retrieve values simultaneously", 259 }, { 260 about: "invalid reset keys", 261 args: []string{"application", "--reset", "reset,bad=key"}, 262 expectError: `--reset accepts a comma delimited set of keys "a,b,c", received: "bad=key"`, 263 }, { 264 about: "init too many args fails", 265 args: []string{"application", "key", "another"}, 266 expectError: "can only retrieve a single value, or all values", 267 }, { 268 about: "--generation with no value", 269 args: []string{"application", "key", "--generation"}, 270 expectError: "option needs an argument: --generation", 271 }, { 272 about: "--generation with invalid value", 273 args: []string{"application", "key", "--generation", "not-there"}, 274 expectError: `generation option must be "current" or "next"`, 275 }} 276 277 func (s *configCommandSuite) TestSetCommandInitError(c *gc.C) { 278 testStore := jujuclienttesting.MinimalStore() 279 for i, test := range setCommandInitErrorTests { 280 c.Logf("test %d: %s", i, test.about) 281 cmd := application.NewConfigCommandForTest(s.fake, s.store) 282 cmd.SetClientStore(testStore) 283 err := cmdtesting.InitCommand(cmd, test.args) 284 c.Assert(err, gc.ErrorMatches, test.expectError) 285 } 286 } 287 288 func (s *configCommandSuite) TestSetCharmConfigSuccess(c *gc.C) { 289 s.assertSetSuccess(c, s.dir, []string{ 290 "username=hello", 291 "outlook=hello@world.tld", 292 }, s.defaultAppValues, map[string]interface{}{ 293 "username": "hello", 294 "outlook": "hello@world.tld", 295 }) 296 s.assertSetSuccess(c, s.dir, []string{ 297 "username=hello=foo", 298 }, s.defaultAppValues, map[string]interface{}{ 299 "username": "hello=foo", 300 "outlook": "hello@world.tld", 301 }) 302 s.assertSetSuccess(c, s.dir, []string{ 303 "username=@valid.txt", 304 }, s.defaultAppValues, map[string]interface{}{ 305 "username": validSetTestValue, 306 "outlook": "hello@world.tld", 307 }) 308 s.assertSetSuccess(c, s.dir, []string{ 309 "username=", 310 }, s.defaultAppValues, map[string]interface{}{ 311 "username": "", 312 "outlook": "hello@world.tld", 313 }) 314 s.assertSetSuccess(c, s.dir, []string{ 315 "--generation", 316 "current", 317 "username=hello", 318 "outlook=hello@world.tld", 319 }, s.defaultAppValues, map[string]interface{}{ 320 "username": "hello", 321 "outlook": "hello@world.tld", 322 }) 323 324 s.fake.generation = model.GenerationNext 325 s.assertSetSuccess(c, s.dir, []string{ 326 "username=hello", 327 "outlook=hello@world.tld", 328 "--generation", 329 "next", 330 }, s.defaultAppValues, map[string]interface{}{ 331 "username": "hello", 332 "outlook": "hello@world.tld", 333 }) 334 } 335 336 func (s *configCommandSuite) TestSetAppConfigSuccess(c *gc.C) { 337 s.assertSetSuccess(c, s.dir, []string{ 338 "juju-external-hostname=hello", 339 }, map[string]interface{}{ 340 "juju-external-hostname": "hello", 341 }, s.defaultCharmValues) 342 s.assertSetSuccess(c, s.dir, []string{ 343 "juju-external-hostname=", 344 }, map[string]interface{}{ 345 "juju-external-hostname": "", 346 }, s.defaultCharmValues) 347 } 348 349 func (s *configCommandSuite) TestSetSameValue(c *gc.C) { 350 s.assertSetSuccess(c, s.dir, []string{ 351 "username=hello", 352 "outlook=hello@world.tld", 353 }, s.defaultAppValues, map[string]interface{}{ 354 "username": "hello", 355 "outlook": "hello@world.tld", 356 }) 357 s.assertSetWarning(c, s.dir, []string{ 358 "username=hello", 359 }, "the configuration setting \"username\" already has the value \"hello\"") 360 s.assertSetWarning(c, s.dir, []string{ 361 "outlook=hello@world.tld", 362 }, "the configuration setting \"outlook\" already has the value \"hello@world.tld\"") 363 364 } 365 366 func (s *configCommandSuite) TestSetConfigFail(c *gc.C) { 367 s.assertSetFail(c, s.dir, []string{"foo", "bar"}, 368 "can only retrieve a single value, or all values") 369 s.assertSetFail(c, s.dir, []string{"=bar"}, "expected \"key=value\", got \"=bar\"") 370 s.assertSetFail(c, s.dir, []string{ 371 "username=@missing.txt", 372 }, "cannot read option from file \"missing.txt\": .* "+utils.NoSuchFileErrRegexp) 373 s.assertSetFail(c, s.dir, []string{ 374 "username=@big.txt", 375 }, "size of option file is larger than 5M") 376 s.assertSetFail(c, s.dir, []string{ 377 "username=@invalid.txt", 378 }, "value for option \"username\" contains non-UTF-8 sequences") 379 } 380 381 func (s *configCommandSuite) TestSetCharmConfigFromYAML(c *gc.C) { 382 s.assertSetFail(c, s.dir, []string{ 383 "--file", 384 "missing.yaml", 385 }, ".*"+utils.NoSuchFileErrRegexp) 386 387 ctx := cmdtesting.ContextForDir(c, s.dir) 388 code := cmd.Main(application.NewConfigCommandForTest(s.fake, s.store), ctx, []string{ 389 "dummy-application", 390 "--file", 391 "testconfig.yaml"}) 392 393 c.Check(code, gc.Equals, 0) 394 c.Check(s.fake.config, gc.Equals, yamlConfigValue) 395 } 396 397 func (s *configCommandSuite) TestSetFromStdin(c *gc.C) { 398 s.fake = &fakeApplicationAPI{name: "dummy-application"} 399 ctx := cmdtesting.Context(c) 400 ctx.Stdin = strings.NewReader("settings:\n username:\n value: world\n") 401 code := cmd.Main(application.NewConfigCommandForTest(s.fake, s.store), ctx, []string{ 402 "dummy-application", 403 "--file", 404 "-"}) 405 406 c.Check(code, gc.Equals, 0) 407 c.Check(s.fake.config, jc.DeepEquals, "settings:\n username:\n value: world\n") 408 } 409 410 func (s *configCommandSuite) TestResetCharmConfigToDefault(c *gc.C) { 411 s.fake = &fakeApplicationAPI{name: "dummy-application", charmValues: map[string]interface{}{ 412 "username": "hello", 413 }} 414 s.assertResetSuccess(c, s.dir, []string{ 415 "--reset", 416 "username", 417 }, nil, make(map[string]interface{})) 418 } 419 420 func (s *configCommandSuite) TestResetAppConfig(c *gc.C) { 421 s.fake = &fakeApplicationAPI{name: "dummy-application", appValues: map[string]interface{}{ 422 "juju-external-hostname": "app-value", 423 }} 424 s.assertResetSuccess(c, s.dir, []string{ 425 "--reset", 426 "juju-external-hostname", 427 }, make(map[string]interface{}), nil) 428 } 429 430 func (s *configCommandSuite) TestBlockSetConfig(c *gc.C) { 431 // Block operation 432 s.fake.err = common.OperationBlockedError("TestBlockSetConfig") 433 cmd := application.NewConfigCommandForTest(s.fake, s.store) 434 cmd.SetClientStore(jujuclienttesting.MinimalStore()) 435 _, err := cmdtesting.RunCommandInDir(c, cmd, []string{ 436 "dummy-application", 437 "--file", 438 "testconfig.yaml", 439 }, s.dir) 440 c.Assert(err, gc.ErrorMatches, `(.|\n)*All operations that change model have been disabled(.|\n)*`) 441 c.Check(c.GetTestLog(), gc.Matches, "(.|\n)*TestBlockSetConfig(.|\n)*") 442 } 443 444 // assertSetSuccess sets configuration options and checks the expected settings. 445 func (s *configCommandSuite) assertSetSuccess( 446 c *gc.C, dir string, args []string, 447 expectAppValues map[string]interface{}, expectCharmValues map[string]interface{}, 448 ) { 449 cmd := application.NewConfigCommandForTest(s.fake, s.store) 450 cmd.SetClientStore(jujuclienttesting.MinimalStore()) 451 452 args = append([]string{"dummy-application"}, args...) 453 _, err := cmdtesting.RunCommandInDir(c, cmd, args, dir) 454 c.Assert(err, jc.ErrorIsNil) 455 appValues := make(map[string]interface{}) 456 for k, v := range s.defaultAppValues { 457 appValues[k] = v 458 } 459 for k, v := range expectAppValues { 460 appValues[k] = v 461 } 462 c.Assert(s.fake.appValues, jc.DeepEquals, appValues) 463 464 charmValues := make(map[string]interface{}) 465 for k, v := range s.defaultCharmValues { 466 charmValues[k] = v 467 } 468 for k, v := range expectCharmValues { 469 charmValues[k] = v 470 } 471 c.Assert(s.fake.charmValues, jc.DeepEquals, charmValues) 472 } 473 474 func (s *configCommandSuite) assertResetSuccess( 475 c *gc.C, dir string, args []string, 476 expectAppValues map[string]interface{}, expectCharmValues map[string]interface{}, 477 ) { 478 cmd := application.NewConfigCommandForTest(s.fake, s.store) 479 cmd.SetClientStore(jujuclienttesting.MinimalStore()) 480 481 args = append([]string{"dummy-application"}, args...) 482 _, err := cmdtesting.RunCommandInDir(c, cmd, args, dir) 483 c.Assert(err, jc.ErrorIsNil) 484 c.Assert(s.fake.appValues, jc.DeepEquals, expectAppValues) 485 c.Assert(s.fake.charmValues, jc.DeepEquals, expectCharmValues) 486 } 487 488 // assertSetFail sets configuration options and checks the expected error. 489 func (s *configCommandSuite) assertSetFail(c *gc.C, dir string, args []string, expectErr string) { 490 cmd := application.NewConfigCommandForTest(s.fake, s.store) 491 cmd.SetClientStore(jujuclienttesting.MinimalStore()) 492 493 args = append([]string{"dummy-application"}, args...) 494 _, err := cmdtesting.RunCommandInDir(c, cmd, args, dir) 495 c.Assert(err, gc.ErrorMatches, expectErr) 496 } 497 498 func (s *configCommandSuite) assertSetWarning(c *gc.C, dir string, args []string, w string) { 499 cmd := application.NewConfigCommandForTest(s.fake, s.store) 500 cmd.SetClientStore(jujuclienttesting.MinimalStore()) 501 _, err := cmdtesting.RunCommandInDir(c, cmd, append([]string{"dummy-application"}, args...), dir) 502 c.Assert(err, jc.ErrorIsNil) 503 c.Assert(strings.Replace(c.GetTestLog(), "\n", " ", -1), gc.Matches, ".*WARNING.*"+w+".*") 504 } 505 506 // setupValueFile creates a file containing one value for testing 507 // set with name=@filename. 508 func setupValueFile(c *gc.C, dir, filename, value string) string { 509 ctx := cmdtesting.ContextForDir(c, dir) 510 path := ctx.AbsPath(filename) 511 content := []byte(value) 512 err := ioutil.WriteFile(path, content, 0666) 513 c.Assert(err, jc.ErrorIsNil) 514 return path 515 } 516 517 // setupBigFile creates a too big file for testing 518 // set with name=@filename. 519 func setupBigFile(c *gc.C, dir string) string { 520 ctx := cmdtesting.ContextForDir(c, dir) 521 path := ctx.AbsPath("big.txt") 522 file, err := os.Create(path) 523 c.Assert(err, jc.ErrorIsNil) 524 defer file.Close() 525 chunk := make([]byte, 1024) 526 for i := 0; i < cap(chunk); i++ { 527 chunk[i] = byte(i % 256) 528 } 529 for i := 0; i < 6000; i++ { 530 _, err = file.Write(chunk) 531 c.Assert(err, jc.ErrorIsNil) 532 } 533 return path 534 } 535 536 // setupConfigFile creates a configuration file for testing set 537 // with the --file argument specifying a configuration file. 538 func setupConfigFile(c *gc.C, dir string) string { 539 ctx := cmdtesting.ContextForDir(c, dir) 540 path := ctx.AbsPath("testconfig.yaml") 541 content := []byte(yamlConfigValue) 542 err := ioutil.WriteFile(path, content, 0666) 543 c.Assert(err, jc.ErrorIsNil) 544 return path 545 }