github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/action/run_test.go (about) 1 // Copyright 2014, 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package action_test 5 6 import ( 7 "bytes" 8 "strings" 9 "unicode/utf8" 10 11 "github.com/juju/cmd/cmdtesting" 12 "github.com/juju/errors" 13 jc "github.com/juju/testing/checkers" 14 "github.com/juju/utils" 15 gc "gopkg.in/check.v1" 16 "gopkg.in/juju/names.v2" 17 "gopkg.in/yaml.v2" 18 19 "github.com/juju/juju/apiserver/common" 20 "github.com/juju/juju/apiserver/params" 21 "github.com/juju/juju/cmd/juju/action" 22 ) 23 24 var ( 25 validParamsYaml = ` 26 out: name 27 compression: 28 kind: xz 29 quality: high 30 `[1:] 31 invalidParamsYaml = ` 32 broken-map: 33 foo: 34 foo 35 bar: baz 36 `[1:] 37 invalidUTFYaml = "out: ok" + string([]byte{0xFF, 0xFF}) 38 ) 39 40 type RunSuite struct { 41 BaseActionSuite 42 dir string 43 } 44 45 var _ = gc.Suite(&RunSuite{}) 46 47 func (s *RunSuite) SetUpTest(c *gc.C) { 48 s.BaseActionSuite.SetUpTest(c) 49 s.dir = c.MkDir() 50 c.Assert(utf8.ValidString(validParamsYaml), jc.IsTrue) 51 c.Assert(utf8.ValidString(invalidParamsYaml), jc.IsTrue) 52 c.Assert(utf8.ValidString(invalidUTFYaml), jc.IsFalse) 53 setupValueFile(c, s.dir, "validParams.yml", validParamsYaml) 54 setupValueFile(c, s.dir, "invalidParams.yml", invalidParamsYaml) 55 setupValueFile(c, s.dir, "invalidUTF.yml", invalidUTFYaml) 56 } 57 58 func (s *RunSuite) TestInit(c *gc.C) { 59 tests := []struct { 60 should string 61 args []string 62 expectUnits []string 63 expectAction string 64 expectParamsYamlPath string 65 expectParseStrings bool 66 expectKVArgs [][]string 67 expectOutput string 68 expectError string 69 }{{ 70 should: "fail with missing args", 71 args: []string{}, 72 expectError: "no unit specified", 73 }, { 74 should: "fail with no action specified", 75 args: []string{validUnitId}, 76 expectError: "no action specified", 77 }, { 78 should: "fail with invalid unit ID", 79 args: []string{invalidUnitId, "valid-action-name"}, 80 expectError: "invalid unit or action name \"something-strange-\"", 81 }, { 82 should: "fail with invalid unit ID first", 83 args: []string{validUnitId, invalidUnitId, "valid-action-name"}, 84 expectError: "invalid unit or action name \"something-strange-\"", 85 }, { 86 should: "fail with invalid unit ID second", 87 args: []string{invalidUnitId, validUnitId, "valid-action-name"}, 88 expectError: "invalid unit or action name \"something-strange-\"", 89 }, { 90 should: "work with multiple valid units", 91 args: []string{validUnitId, validUnitId2, "valid-action-name"}, 92 expectUnits: []string{validUnitId, validUnitId2}, 93 expectAction: "valid-action-name", 94 expectKVArgs: [][]string{}, 95 }, {}, { 96 should: "fail with invalid action name", 97 args: []string{validUnitId, "BadName"}, 98 expectError: "invalid unit or action name \"BadName\"", 99 }, { 100 should: "fail with invalid action name ending in \"-\"", 101 args: []string{validUnitId, "name-end-with-dash-"}, 102 expectError: "invalid unit or action name \"name-end-with-dash-\"", 103 }, { 104 should: "fail with wrong formatting of k-v args", 105 args: []string{validUnitId, "valid-action-name", "uh"}, 106 expectError: "argument \"uh\" must be of the form key...=value", 107 }, { 108 should: "fail with wrong formatting of k-v args", 109 args: []string{validUnitId, "valid-action-name", "foo.Baz=3"}, 110 expectError: "key \"Baz\" must start and end with lowercase alphanumeric, and contain only lowercase alphanumeric and hyphens", 111 }, { 112 should: "fail with wrong formatting of k-v args", 113 args: []string{validUnitId, "valid-action-name", "no-go?od=3"}, 114 expectError: "key \"no-go\\?od\" must start and end with lowercase alphanumeric, and contain only lowercase alphanumeric and hyphens", 115 }, { 116 should: "work with action name ending in numeric values", 117 args: []string{validUnitId, "action-01"}, 118 expectUnits: []string{validUnitId}, 119 expectAction: "action-01", 120 }, { 121 should: "work with numeric values within action name", 122 args: []string{validUnitId, "action-00-foo"}, 123 expectUnits: []string{validUnitId}, 124 expectAction: "action-00-foo", 125 }, { 126 should: "work with action name starting with numeric values", 127 args: []string{validUnitId, "00-action"}, 128 expectUnits: []string{validUnitId}, 129 expectAction: "00-action", 130 }, { 131 should: "work with empty values", 132 args: []string{validUnitId, "valid-action-name", "ok="}, 133 expectUnits: []string{validUnitId}, 134 expectAction: "valid-action-name", 135 expectKVArgs: [][]string{{"ok", ""}}, 136 }, { 137 should: "handle --parse-strings", 138 args: []string{validUnitId, "valid-action-name", "--string-args"}, 139 expectUnits: []string{validUnitId}, 140 expectAction: "valid-action-name", 141 expectParseStrings: true, 142 }, { 143 // cf. worker/uniter/runner/jujuc/action-set_test.go per @fwereade 144 should: "work with multiple '=' signs", 145 args: []string{validUnitId, "valid-action-name", "ok=this=is=weird="}, 146 expectUnits: []string{validUnitId}, 147 expectAction: "valid-action-name", 148 expectKVArgs: [][]string{{"ok", "this=is=weird="}}, 149 }, { 150 should: "init properly with no params", 151 args: []string{validUnitId, "valid-action-name"}, 152 expectUnits: []string{validUnitId}, 153 expectAction: "valid-action-name", 154 }, { 155 should: "handle --params properly", 156 args: []string{validUnitId, "valid-action-name", "--params=foo.yml"}, 157 expectUnits: []string{validUnitId}, 158 expectAction: "valid-action-name", 159 expectParamsYamlPath: "foo.yml", 160 }, { 161 should: "handle --params and key-value args", 162 args: []string{ 163 validUnitId, 164 "valid-action-name", 165 "--params=foo.yml", 166 "foo.bar=2", 167 "foo.baz.bo=3", 168 "bar.foo=hello", 169 }, 170 expectUnits: []string{validUnitId}, 171 expectAction: "valid-action-name", 172 expectParamsYamlPath: "foo.yml", 173 expectKVArgs: [][]string{ 174 {"foo", "bar", "2"}, 175 {"foo", "baz", "bo", "3"}, 176 {"bar", "foo", "hello"}, 177 }, 178 }, { 179 should: "handle key-value args with no --params", 180 args: []string{ 181 validUnitId, 182 "valid-action-name", 183 "foo.bar=2", 184 "foo.baz.bo=y", 185 "bar.foo=hello", 186 }, 187 expectUnits: []string{validUnitId}, 188 expectAction: "valid-action-name", 189 expectKVArgs: [][]string{ 190 {"foo", "bar", "2"}, 191 {"foo", "baz", "bo", "y"}, 192 {"bar", "foo", "hello"}, 193 }, 194 }, { 195 should: "work with leader identifier", 196 args: []string{"mysql/leader", "valid-action-name"}, 197 expectUnits: []string{"mysql/leader"}, 198 expectAction: "valid-action-name", 199 expectKVArgs: [][]string{}, 200 }} 201 202 for i, t := range tests { 203 for _, modelFlag := range s.modelFlags { 204 wrappedCommand, command := action.NewRunCommandForTest(s.store) 205 c.Logf("test %d: should %s:\n$ juju run-action %s\n", i, 206 t.should, strings.Join(t.args, " ")) 207 args := append([]string{modelFlag, "admin"}, t.args...) 208 err := cmdtesting.InitCommand(wrappedCommand, args) 209 if t.expectError == "" { 210 c.Check(command.UnitNames(), gc.DeepEquals, t.expectUnits) 211 c.Check(command.ActionName(), gc.Equals, t.expectAction) 212 c.Check(command.ParamsYAML().Path, gc.Equals, t.expectParamsYamlPath) 213 c.Check(command.Args(), jc.DeepEquals, t.expectKVArgs) 214 c.Check(command.ParseStrings(), gc.Equals, t.expectParseStrings) 215 } else { 216 c.Check(err, gc.ErrorMatches, t.expectError) 217 } 218 } 219 } 220 } 221 222 func (s *RunSuite) TestRun(c *gc.C) { 223 tests := []struct { 224 should string 225 clientSetup func(client *fakeAPIClient) 226 withArgs []string 227 withAPIErr error 228 withActionResults []params.ActionResult 229 expectedActionEnqueued params.Action 230 expectedErr string 231 }{{ 232 should: "fail with multiple results", 233 withArgs: []string{validUnitId, "some-action"}, 234 withActionResults: []params.ActionResult{ 235 {Action: ¶ms.Action{Tag: validActionTagString}}, 236 {Action: ¶ms.Action{Tag: validActionTagString}}, 237 }, 238 expectedErr: "illegal number of results returned", 239 }, { 240 should: "fail with API error", 241 withArgs: []string{validUnitId, "some-action"}, 242 withActionResults: []params.ActionResult{{ 243 Action: ¶ms.Action{Tag: validActionTagString}}, 244 }, 245 withAPIErr: errors.New("something wrong in API"), 246 expectedErr: "something wrong in API", 247 }, { 248 should: "fail with error in result", 249 withArgs: []string{validUnitId, "some-action"}, 250 withActionResults: []params.ActionResult{{ 251 Action: ¶ms.Action{Tag: validActionTagString}, 252 Error: common.ServerError(errors.New("database error")), 253 }}, 254 expectedErr: "database error", 255 }, { 256 should: "fail with invalid tag in result", 257 withArgs: []string{validUnitId, "some-action"}, 258 withActionResults: []params.ActionResult{{ 259 Action: ¶ms.Action{Tag: invalidActionTagString}, 260 }}, 261 expectedErr: "\"" + invalidActionTagString + "\" is not a valid action tag", 262 }, { 263 should: "fail with missing file passed", 264 withArgs: []string{validUnitId, "some-action", 265 "--params", s.dir + "/" + "missing.yml", 266 }, 267 expectedErr: "open .*missing.yml: " + utils.NoSuchFileErrRegexp, 268 }, { 269 should: "fail with invalid yaml in file", 270 withArgs: []string{validUnitId, "some-action", 271 "--params", s.dir + "/" + "invalidParams.yml", 272 }, 273 expectedErr: "yaml: line 4: mapping values are not allowed in this context", 274 }, { 275 should: "fail with invalid UTF in file", 276 withArgs: []string{validUnitId, "some-action", 277 "--params", s.dir + "/" + "invalidUTF.yml", 278 }, 279 expectedErr: "yaml: invalid leading UTF-8 octet", 280 }, { 281 should: "fail with invalid YAML passed as arg and no --string-args", 282 withArgs: []string{validUnitId, "some-action", "foo.bar=\""}, 283 expectedErr: "yaml: found unexpected end of stream", 284 }, { 285 should: "enqueue a basic action with no params", 286 withArgs: []string{validUnitId, "some-action"}, 287 withActionResults: []params.ActionResult{{ 288 Action: ¶ms.Action{Tag: validActionTagString}, 289 }}, 290 expectedActionEnqueued: params.Action{ 291 Name: "some-action", 292 Parameters: map[string]interface{}{}, 293 Receiver: names.NewUnitTag(validUnitId).String(), 294 }, 295 }, { 296 should: "enqueue an action with some explicit params", 297 withArgs: []string{validUnitId, "some-action", 298 "out.name=bar", 299 "out.kind=tmpfs", 300 "out.num=3", 301 "out.boolval=y", 302 }, 303 withActionResults: []params.ActionResult{{ 304 Action: ¶ms.Action{Tag: validActionTagString}, 305 }}, 306 expectedActionEnqueued: params.Action{ 307 Name: "some-action", 308 Receiver: names.NewUnitTag(validUnitId).String(), 309 Parameters: map[string]interface{}{ 310 "out": map[string]interface{}{ 311 "name": "bar", 312 "kind": "tmpfs", 313 "num": 3, 314 "boolval": true, 315 }, 316 }, 317 }, 318 }, { 319 should: "enqueue an action with some raw string params", 320 withArgs: []string{validUnitId, "some-action", "--string-args", 321 "out.name=bar", 322 "out.kind=tmpfs", 323 "out.num=3", 324 "out.boolval=y", 325 }, 326 withActionResults: []params.ActionResult{{ 327 Action: ¶ms.Action{Tag: validActionTagString}, 328 }}, 329 expectedActionEnqueued: params.Action{ 330 Name: "some-action", 331 Receiver: names.NewUnitTag(validUnitId).String(), 332 Parameters: map[string]interface{}{ 333 "out": map[string]interface{}{ 334 "name": "bar", 335 "kind": "tmpfs", 336 "num": "3", 337 "boolval": "y", 338 }, 339 }, 340 }, 341 }, { 342 should: "enqueue an action with file params plus CLI args", 343 withArgs: []string{validUnitId, "some-action", 344 "--params", s.dir + "/" + "validParams.yml", 345 "compression.kind=gz", 346 "compression.fast=true", 347 }, 348 withActionResults: []params.ActionResult{{ 349 Action: ¶ms.Action{Tag: validActionTagString}, 350 }}, 351 expectedActionEnqueued: params.Action{ 352 Name: "some-action", 353 Receiver: names.NewUnitTag(validUnitId).String(), 354 Parameters: map[string]interface{}{ 355 "out": "name", 356 "compression": map[string]interface{}{ 357 "kind": "gz", 358 "quality": "high", 359 "fast": true, 360 }, 361 }, 362 }, 363 }, { 364 should: "enqueue an action with file params and explicit params", 365 withArgs: []string{validUnitId, "some-action", 366 "out.name=bar", 367 "out.kind=tmpfs", 368 "compression.quality.speed=high", 369 "compression.quality.size=small", 370 "--params", s.dir + "/" + "validParams.yml", 371 }, 372 withActionResults: []params.ActionResult{{ 373 Action: ¶ms.Action{Tag: validActionTagString}, 374 }}, 375 expectedActionEnqueued: params.Action{ 376 Name: "some-action", 377 Receiver: names.NewUnitTag(validUnitId).String(), 378 Parameters: map[string]interface{}{ 379 "out": map[string]interface{}{ 380 "name": "bar", 381 "kind": "tmpfs", 382 }, 383 "compression": map[string]interface{}{ 384 "kind": "xz", 385 "quality": map[string]interface{}{ 386 "speed": "high", 387 "size": "small", 388 }, 389 }, 390 }, 391 }, 392 }, { 393 should: "fail with not implemented Leaders method", 394 withArgs: []string{"mysql/leader", "some-action"}, 395 withActionResults: []params.ActionResult{{ 396 Action: ¶ms.Action{Tag: validActionTagString}}, 397 }, 398 expectedErr: "unable to determine leader for application \"mysql\"" + 399 "\nleader determination is unsupported by this API" + 400 "\neither upgrade your controller, or explicitly specify a unit", 401 }, { 402 should: "enqueue a basic action on the leader", 403 clientSetup: func(api *fakeAPIClient) { api.apiVersion = 3 }, 404 withArgs: []string{"mysql/leader", "some-action"}, 405 withActionResults: []params.ActionResult{{ 406 Action: ¶ms.Action{Tag: validActionTagString}, 407 }}, 408 expectedActionEnqueued: params.Action{ 409 Name: "some-action", 410 Parameters: map[string]interface{}{}, 411 Receiver: "mysql/leader", 412 }, 413 }} 414 415 for i, t := range tests { 416 for _, modelFlag := range s.modelFlags { 417 func() { 418 c.Logf("test %d: should %s:\n$ juju actions do %s\n", i, t.should, strings.Join(t.withArgs, " ")) 419 420 fakeClient := &fakeAPIClient{ 421 actionResults: t.withActionResults, 422 apiVersion: 2, 423 } 424 if t.clientSetup != nil { 425 t.clientSetup(fakeClient) 426 } 427 428 fakeClient.apiErr = t.withAPIErr 429 restore := s.patchAPIClient(fakeClient) 430 defer restore() 431 432 wrappedCommand, _ := action.NewRunCommandForTest(s.store) 433 args := append([]string{modelFlag, "admin"}, t.withArgs...) 434 ctx, err := cmdtesting.RunCommand(c, wrappedCommand, args...) 435 436 if t.expectedErr != "" || t.withAPIErr != nil { 437 c.Check(err, gc.ErrorMatches, t.expectedErr) 438 } else { 439 c.Assert(err, gc.IsNil) 440 // Before comparing, double-check to avoid 441 // panics in malformed tests. 442 c.Assert(len(t.withActionResults), gc.Equals, 1) 443 // Make sure the test's expected Action was 444 // non-nil and correct. 445 c.Assert(t.withActionResults[0].Action, gc.NotNil) 446 expectedTag, err := names.ParseActionTag(t.withActionResults[0].Action.Tag) 447 c.Assert(err, gc.IsNil) 448 449 // Make sure the CLI responded with the expected tag 450 outputResult := ctx.Stdout.(*bytes.Buffer).Bytes() 451 resultMap := make(map[string]string) 452 err = yaml.Unmarshal(outputResult, &resultMap) 453 c.Assert(err, gc.IsNil) 454 c.Check(resultMap["Action queued with id"], jc.DeepEquals, expectedTag.Id()) 455 456 // Make sure the Action sent to the API to be 457 // enqueued was indeed the expected map 458 enqueued := fakeClient.EnqueuedActions() 459 c.Assert(enqueued.Actions, gc.HasLen, 1) 460 c.Check(enqueued.Actions[0], jc.DeepEquals, t.expectedActionEnqueued) 461 } 462 }() 463 } 464 } 465 }