Repository: StackStorm/st2

Issue:
st2 run --inherit-env does not work for actions run with python-script runner
##### SUMMARY

The `--inherit-env` option supposedly supports the `python-script` runner:

```
  -e, --inherit-env     Pass all the environment variables which are
                        accessible to the CLI as "env" parameter to the
                        action. Note: Only works with python, local and remote
                        runners.
```

But it doesn't actually pass the environment variables to the Python script. I suspect that this has been broken for some time.

The `core.local` and `core.remote` actions do pass their environment variables correctly.

##### ISSUE TYPE
Pick one below and delete the rest:
 - Bug Report

<!--- If you are looking for help, check forum.stackstorm.com. -->

##### STACKSTORM VERSION

`st2 3.1dev (95d7cc3), on Python 2.7.12`

##### OS / ENVIRONMENT / INSTALL METHOD
<!--- Post what OS you are running this on, along with any other relevant information
 - e.g. Docker, Vagrant, Kubernetes, etc. Describe how you installed ST2
 - e.g. one-line install, custom install, etc -->

Ubuntu 16.04, installed as part of `setup_e2e_tests` in st2ci pack.

##### STEPS TO REPRODUCE
<!--- For bugs, show how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used. -->

<!--- Paste example actions and workflows between quotes below -->
`actions/test_inherit_env_python_script_runner.yaml`:
```yaml
---
name: test_inherit_env_python_script_runner
description: Test the --inherit-env option to the st2 command for an action run with the python-script runner
enabled: true
runner_type: python-script
entry_point: test_inherit_env.py
parameters: {}
```

`actions/test_inherit_env.py`:
```python
import os

from st2common.runners.base_action import Action


class InheritEnvTestAction(Action):
    def run(self, *args, **kwargs):
        return (True, os.environ)
```

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

```bash
env | grep SLACK_; st2 run --inherit-env tests.test_inherit_env_python_script_runner
```

```
SLACK_USER_API_TOKEN=xoxb-...
SLACK_CHANNEL=chatops_e2e_tests_u16
SLACK_BOT_USERNAME=chatops_test_bot_u16
SLACK_USER_USERNAME=chatops_test_user_u16
.
id: ...
status: succeeded
parameters: None
result:
  exit_code: 0
  result: '{
    ''SLACK_USER_API_TOKEN'': ''xoxb-...'',
    ''SLACK_CHANNEL'': ''chatops_e2e_tests_u16'',
    ''SLACK_BOT_USERNAME'': ''chatops_test_bot_u16'',
    ''SLACK_USER_USERNAME'': ''chatops_test_user_u16'',
    ''ST2_ACTION_EXECUTION_ID'': ''...',
    ''DAEMON_ARGS'': ''--config-file /etc/st2/st2.conf'',
    ''ST2_ACTION_API_URL'': ''http://127.0.0.1:9101/v1'',
    ''ST2_ACTION_AUTH_TOKEN'': ''...'',
    ''PYTHONPATH'': ''/opt/stackstorm/packs/tests/lib:/opt/stackstorm/st2/lib/python2.7/site-packages'',
    ''ST2_AUTH_TOKEN'': ''...'',
    ''SHELL'': ''/bin/sh'',
    ''LOGNAME'': ''root'',
    ''ST2_API_URL'': ''http://127.0.0.1:9101/v1'',
    ''WORKERID'': ''10'',
    ''PATH'': ''/opt/stackstorm/virtualenvs/tests/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'',
    ''HOME'': ''/root'',
    ''USER'': ''root''
  }'
  stderr: ''
  stdout: ''
```

Note how the `SLACK_` environment variables show up in the output from st2.

##### ACTUAL RESULTS
<!--- What happened? What output did you get? -->

```bash
env | grep SLACK_; st2 run --inherit-env tests.test_inherit_env_python_script_runner
```

```
SLACK_USER_API_TOKEN=xoxb-...
SLACK_CHANNEL=chatops_e2e_tests_u16
SLACK_BOT_USERNAME=chatops_test_bot_u16
SLACK_USER_USERNAME=chatops_test_user_u16
.
id: ...
status: succeeded
parameters: None
result:
  exit_code: 0
  result: '{
    ''ST2_ACTION_PACK_NAME'': ''tests'',
    ''ST2_ACTION_EXECUTION_ID'': ''...',
    ''DAEMON_ARGS'': ''--config-file /etc/st2/st2.conf'',
    ''ST2_ACTION_API_URL'': ''http://127.0.0.1:9101/v1'',
    ''ST2_ACTION_AUTH_TOKEN'': ''...'',
    ''PYTHONPATH'': ''/opt/stackstorm/packs/tests/lib:/opt/stackstorm/st2/lib/python2.7/site-packages'',
    ''ST2_AUTH_TOKEN'': ''...'',
    ''SHELL'': ''/bin/sh'',
    ''LOGNAME'': ''root'',
    ''ST2_API_URL'': ''http://127.0.0.1:9101/v1'',
    ''WORKERID'': ''10'',
    ''PATH'': ''/opt/stackstorm/virtualenvs/tests/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'',
    ''HOME'': ''/root'',
    ''USER'': ''root''
  }'
  stderr: ''
  stdout: ''
```
Note how the `SLACK_` environment variables do not show up in the output from st2.


Discussion:
Reported for Ubuntu Xenial but still applies to later and currently supported OS. 

Resolving patch:
diff --git a/st2client/st2client/commands/action.py b/st2client/st2client/commands/action.py
--- a/st2client/st2client/commands/action.py
+++ b/st2client/st2client/commands/action.py
@@ -945,6 +945,9 @@ def normalize(name, value, action_params=None, auto_dict=False):
 
         result = {}
 
+        if args.inherit_env:
+            result["env"] = self._get_inherited_env_vars()
+
         if not args.parameters:
             return result
 
@@ -1008,9 +1011,6 @@ def normalize(name, value, action_params=None, auto_dict=False):
 
             del result["_file_name"]
 
-        if args.inherit_env:
-            result["env"] = self._get_inherited_env_vars()
-
         return result
 
     @add_auth_token_to_kwargs_from_cli


Test patch:
diff --git a/st2client/tests/unit/test_command_actionrun.py b/st2client/tests/unit/test_command_actionrun.py
--- a/st2client/tests/unit/test_command_actionrun.py
+++ b/st2client/tests/unit/test_command_actionrun.py
@@ -261,3 +261,135 @@ def test_get_params_from_args_with_multiple_declarations(self):
 
         # set auto_dict back to default
         mockarg.auto_dict = False
+
+    def test_correctly_process_inherit_env_when_no_parameters_set(self):
+        """test_correctly_process_inherit_env_when_no_parameters_set
+
+        This tests that we still correctly pass through the environment variables
+        when --inherit-env is set and we run an action that does not have parameters
+        """
+
+        runner = RunnerType()
+        runner.runner_parameters = {}
+
+        action = Action()
+        action.ref = "test.action"
+
+        subparser = mock.Mock()
+        command = ActionRunCommand(action, self, subparser, name="test")
+
+        mockarg = mock.Mock()
+        mockarg.inherit_env = True
+        mockarg.auto_dict = True
+        mockarg.parameters = []
+
+        k1 = "key1"
+        v1 = "value1"
+        k2 = "key2"
+        v2 = "value2"
+
+        with mock.patch("os.environ.copy") as mockCopy:
+            mockCopy.return_value = {k1: v1, k2: v2}
+            param = command._get_action_parameters_from_args(
+                action=action, runner=runner, args=mockarg
+            )
+
+        self.assertIn("env", param)
+
+        env_params = param["env"]
+        self.assertIn(k1, env_params)
+        self.assertIn(k2, env_params)
+        self.assertEqual(v1, env_params[k1])
+        self.assertEqual(v2, env_params[k2])
+
+    def test_correctly_process_inherit_env_when_parameters_set(self):
+        """test_correctly_process_inherit_env_when_parameters_set
+
+        This tests that we still correctly pass through the environment variables
+        when --inherit-env is set and we run an action that has action parameters set
+        """
+
+        runner = RunnerType()
+        runner.runner_parameters = {}
+
+        action = Action()
+        action.ref = "test.action"
+        action.parameters = {
+            "param_string": {"type": "string"},
+            "param_array": {"type": "array"},
+            "param_array_of_dicts": {"type": "array"},
+        }
+
+        subparser = mock.Mock()
+        command = ActionRunCommand(action, self, subparser, name="test")
+
+        p_string = "param_string"
+        p_array = "param_array"
+        p_ra_dicts = "param_array_of_dicts"
+        mockarg = mock.Mock()
+        mockarg.inherit_env = True
+        mockarg.auto_dict = True
+        mockarg.parameters = [
+            f"{p_string}=hoge",
+            f"{p_array}=foo,bar",
+            f"{p_ra_dicts}=foo:1,bar:2",
+        ]
+
+        k1 = "key1"
+        v1 = "value1"
+        k2 = "key2"
+        v2 = "value2"
+
+        with mock.patch("os.environ.copy") as mockCopy:
+            mockCopy.return_value = {k1: v1, k2: v2}
+            param = command._get_action_parameters_from_args(
+                action=action, runner=runner, args=mockarg
+            )
+
+        self.assertIn("env", param)
+
+        env_params = param["env"]
+        self.assertIn(k1, env_params)
+        self.assertIn(k2, env_params)
+        self.assertEqual(v1, env_params[k1])
+        self.assertEqual(v2, env_params[k2])
+        self.assertIn(p_string, param)
+        self.assertEqual("hoge", param[p_string])
+        self.assertIn(p_array, param)
+        self.assertIn("foo", param[p_array])
+        self.assertIn("bar", param[p_array])
+        self.assertIn(p_ra_dicts, param)
+        self.assertDictEqual({"foo": "1", "bar": "2"}, param[p_ra_dicts][0])
+
+    def test_correctly_generate_empty_params_no_inherit_empty_parameters(self):
+        """test_correctly_generate_empty_params_no_inherit_empty_parameters
+
+        Verifies that we return an empty dict when we do not provide inherit env and parameters
+        """
+
+        runner = RunnerType()
+        runner.runner_parameters = {}
+
+        action = Action()
+        action.ref = "test.action"
+
+        subparser = mock.Mock()
+        command = ActionRunCommand(action, self, subparser, name="test")
+
+        mockarg = mock.Mock()
+        mockarg.inherit_env = False
+        mockarg.auto_dict = True
+        mockarg.parameters = []
+
+        k1 = "key1"
+        v1 = "value1"
+        k2 = "key2"
+        v2 = "value2"
+
+        with mock.patch("os.environ.copy") as mockCopy:
+            mockCopy.return_value = {k1: v1, k2: v2}
+            param = command._get_action_parameters_from_args(
+                action=action, runner=runner, args=mockarg
+            )
+
+        self.assertDictEqual({}, param)