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: &params.Action{Tag: validActionTagString}},
   236  			{Action: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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  }