Repository: StackStorm/st2

Issue:
Action metadata file uses "parameters" and action chain itself uses "params". This is really confusing!
https://stackstorm-community.slack.com/archives/community/p1451501940009873

Reported by: Fabio Brito on Slack. 



Discussion:
Agreed.

I had the same complaint quite a long time ago, but I don't remember why we didn't change it back then.

In any case, to begin with and make the change backward compatible, we could start with supporting both - `params` and `parameters` attribute. And to avoid confusion and make it explicit, if both are specified we should throw.

Yup +1 on the change. It was a mistake in the first place should simply be `parameters` everywhere.

I can pick up the change.


Resolving patch:
diff --git a/st2actions/st2actions/runners/actionchainrunner.py b/st2actions/st2actions/runners/actionchainrunner.py
--- a/st2actions/st2actions/runners/actionchainrunner.py
+++ b/st2actions/st2actions/runners/actionchainrunner.py
@@ -61,12 +61,15 @@ class ChainHolder(object):
     def __init__(self, chainspec, chainname):
         self.actionchain = actionchain.ActionChain(**chainspec)
         self.chainname = chainname
+
         if not self.actionchain.default:
             default = self._get_default(self.actionchain)
             self.actionchain.default = default
+
         LOG.debug('Using %s as default for %s.', self.actionchain.default, self.chainname)
         if not self.actionchain.default:
             raise Exception('Failed to find default node in %s.' % (self.chainname))
+
         self.vars = {}
 
     def init_vars(self, action_parameters):
@@ -483,7 +486,7 @@ def _resolve_params(action_node, original_parameters, results, chain_vars, chain
         context.update({SYSTEM_KV_PREFIX: KeyValueLookup()})
         context.update({ACTION_CONTEXT_KV_PREFIX: chain_context})
         try:
-            rendered_params = jinja_utils.render_values(mapping=action_node.params,
+            rendered_params = jinja_utils.render_values(mapping=action_node.get_parameters(),
                                                         context=context)
         except Exception as e:
             LOG.exception('Jinja rendering for parameter "%s" failed.' % (e.key))
diff --git a/st2common/st2common/models/system/actionchain.py b/st2common/st2common/models/system/actionchain.py
--- a/st2common/st2common/models/system/actionchain.py
+++ b/st2common/st2common/models/system/actionchain.py
@@ -38,6 +38,12 @@ class Node(object):
                 "required": True
             },
             "params": {
+                "type": "object",
+                "description": ("Parameter for the execution (old name, here for backward "
+                                "compatibility reasons)."),
+                "default": {}
+            },
+            "parameters": {
                 "type": "object",
                 "description": "Parameter for the execution.",
                 "default": {}
@@ -86,6 +92,24 @@ def __init__(self, **kw):
             prop = string.replace(prop, '-', '_')
             setattr(self, prop, value)
 
+    def validate(self):
+        params = getattr(self, 'params', {})
+        parameters = getattr(self, 'parameters', {})
+
+        if params and parameters:
+            msg = ('Either "params" or "parameters" attribute needs to be provided, but not '
+                   'both')
+            raise ValueError(msg)
+
+        return self
+
+    def get_parameters(self):
+        # Note: "params" is old deprecated attribute which will be removed in a future release
+        params = getattr(self, 'params', {})
+        parameters = getattr(self, 'parameters', {})
+
+        return parameters or params
+
     def __repr__(self):
         return ('<Node name=%s, ref=%s, on-success=%s, on-failure=%s>' %
                 (self.name, self.ref, self.on_success, self.on_failure))
@@ -129,6 +153,8 @@ def __init__(self, **kw):
             if prop == 'chain':
                 nodes = []
                 for node in value:
-                    nodes.append(Node(**node))
+                    ac_node = Node(**node)
+                    ac_node.validate()
+                    nodes.append(ac_node)
                 value = nodes
             setattr(self, prop, value)


Test patch:
diff --git a/st2actions/tests/unit/test_actionchain.py b/st2actions/tests/unit/test_actionchain.py
--- a/st2actions/tests/unit/test_actionchain.py
+++ b/st2actions/tests/unit/test_actionchain.py
@@ -101,6 +101,12 @@ def __init__(self, status=LIVEACTION_STATUS_SUCCEEDED, result=''):
     FIXTURES_PACK, 'actionchains', 'chain_publish_params_rendering_failure.yaml')
 CHAIN_WITH_INVALID_ACTION = FixturesLoader().get_fixture_file_path_abs(
     FIXTURES_PACK, 'actionchains', 'chain_with_invalid_action.yaml')
+CHAIN_ACTION_PARAMS_AND_PARAMETERS_ATTRIBUTE = FixturesLoader().get_fixture_file_path_abs(
+    FIXTURES_PACK, 'actionchains', 'chain_action_params_and_parameters.yaml')
+CHAIN_ACTION_PARAMS_ATTRIBUTE = FixturesLoader().get_fixture_file_path_abs(
+    FIXTURES_PACK, 'actionchains', 'chain_action_params_attribute.yaml')
+CHAIN_ACTION_PARAMETERS_ATTRIBUTE = FixturesLoader().get_fixture_file_path_abs(
+    FIXTURES_PACK, 'actionchains', 'chain_action_parameters_attribute.yaml')
 
 CHAIN_NOTIFY_API = {'notify': {'on-complete': {'message': 'foo happened.'}}}
 CHAIN_NOTIFY_DB = NotificationsHelper.to_model(CHAIN_NOTIFY_API)
@@ -678,6 +684,63 @@ def test_action_chain_runner_referenced_action_doesnt_exist(self, mock_request):
         self.assertTrue(expected_error in output['error'])
         self.assertTrue('Traceback' in output['traceback'], output['traceback'])
 
+    def test_exception_is_thrown_if_both_params_and_parameters_attributes_are_provided(self):
+        chain_runner = acr.get_runner()
+        chain_runner.entry_point = CHAIN_ACTION_PARAMS_AND_PARAMETERS_ATTRIBUTE
+        chain_runner.action = ACTION_2
+        chain_runner.container_service = RunnerContainerService()
+
+        expected_msg = ('Either "params" or "parameters" attribute needs to be provided, but '
+                       'not both')
+        self.assertRaisesRegexp(runnerexceptions.ActionRunnerPreRunError, expected_msg,
+                                chain_runner.pre_run)
+
+    @mock.patch.object(action_db_util, 'get_action_by_ref',
+                       mock.MagicMock(return_value=ACTION_1))
+    @mock.patch.object(action_service, 'request', return_value=(DummyActionExecution(), None))
+    def test_params_and_parameters_attributes_both_work(self, _):
+        # "params" attribute used
+        chain_runner = acr.get_runner()
+        chain_runner.entry_point = CHAIN_ACTION_PARAMS_ATTRIBUTE
+        chain_runner.action = ACTION_2
+        chain_runner.container_service = RunnerContainerService()
+        chain_runner.pre_run()
+
+        original_build_liveaction_object = chain_runner._build_liveaction_object
+
+        def mock_build_liveaction_object(action_node, resolved_params, parent_context):
+            # Verify parameters are correctly passed to the action
+            self.assertEqual(resolved_params, {'pparams': 'v1'})
+            original_build_liveaction_object(action_node=action_node,
+                                             resolved_params=resolved_params,
+                                             parent_context=parent_context)
+
+        chain_runner._build_liveaction_object = mock_build_liveaction_object
+
+        action_parameters = {}
+        status, output, _ = chain_runner.run(action_parameters=action_parameters)
+        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
+
+        # "parameters" attribute used
+        chain_runner = acr.get_runner()
+        chain_runner.entry_point = CHAIN_ACTION_PARAMETERS_ATTRIBUTE
+        chain_runner.action = ACTION_2
+        chain_runner.container_service = RunnerContainerService()
+        chain_runner.pre_run()
+
+        def mock_build_liveaction_object(action_node, resolved_params, parent_context):
+            # Verify parameters are correctly passed to the action
+            self.assertEqual(resolved_params, {'pparameters': 'v1'})
+            original_build_liveaction_object(action_node=action_node,
+                                             resolved_params=resolved_params,
+                                             parent_context=parent_context)
+
+        chain_runner._build_liveaction_object = mock_build_liveaction_object
+
+        action_parameters = {}
+        status, output, _ = chain_runner.run(action_parameters=action_parameters)
+        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
+
     @classmethod
     def tearDownClass(cls):
         FixturesLoader().delete_models_from_db(MODELS)