Repository: StackStorm/st2

Issue:
Fail to produce a validation message on an array. validate_config_against_schema gets a TypeError Exception
<!---
Please complete as much of this form as possible. It's OK if you
don't know all the answers, but fill in as much as you can. This will help
us to help you. -->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report *
 - Feature Idea
 - Other

<!--- Is this actually a question? In that case, post it at forum.stackstorm.com. That's a great place
to interact with users and developers, and discuss ideas. -->

##### STACKSTORM VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "st2 --version" between quotes below -->
```
st2 2.8dev (0f1d9dc), on Python 2.7.6
```

##### OS / ENVIRONMENT / INSTALL METHOD
<!--- Below this comment, 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 -->

docker
##### SUMMARY
<!--- Explain the problem or request briefly -->

There is a problem when validating an array of objects. If the validation fails then on this line:

[https://github.com/StackStorm/st2/blob/master/st2common/st2common/util/pack.py#L112
](url)

Because the path has a number you cannot join an integer.


```
Traceback (most recent call last):
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/router.py", line 472, in __call__
    resp = func(**kw)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2api/controllers/v1/pack_configs.py", line 107, in put
    config_api.validate(validate_against_schema=True)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/models/api/pack.py", line 274, in validate
    cleaned_values = self._validate_config_values_against_schema()
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/models/api/pack.py", line 296, in _validate_config_values_against_schema
    pack_name=self.pack)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/util/pack.py", line 111, in validate_config_against_schema
    attribute = '.'.join(attribute)
TypeError: sequence item 1: expected string or Unicode, int found
2018-06-06 14:24:35,673 139995931735056 ERROR error_handling [-] API call failed: sequence item 1: expected string or Unicode, int found
Traceback (most recent call last):

```

When I changed the code to:

```
attribute = '.'.join(str(attribute))

```


MESSAGE: Failed validating attribute "d.e.q.u.e.(.[.u.'.i.n.s.t.a.n.c.e.s.'.,. .0.,. .u.'.a.l.i.a.s.'.].)" in config for pack "my_pack" (/opt/stackstorm/configs/my_pack.yaml): u'alias' is a required property.

I guess there is a problem with path when you have an array becuase you get for example 
instances[0]['alias'] for a path and the exception cannot produce a message







Discussion:
Could it be related to changes in this PR - https://github.com/StackStorm/st2/pull/4122?

@NikosVlagoidis Please also post `config.schema.yaml` and config file for pack in question which reproduces this problem. Just the error message itself is not much help.

On a related note - when is that error returned? When performing an API operation? What operation. Again, please include more information which will help us troubleshoot the issue.

Resolving patch:
diff --git a/st2common/st2common/util/pack.py b/st2common/st2common/util/pack.py
--- a/st2common/st2common/util/pack.py
+++ b/st2common/st2common/util/pack.py
@@ -14,8 +14,10 @@
 # limitations under the License.
 
 from __future__ import absolute_import
+
 import os
 import re
+import collections
 
 from st2common.util import schema as util_schema
 from st2common.constants.pack import MANIFEST_FILE_NAME
@@ -109,7 +111,12 @@ def validate_config_against_schema(config_schema, config_object, config_path,
                                        allow_default_none=True)
     except jsonschema.ValidationError as e:
         attribute = getattr(e, 'path', [])
-        attribute = '.'.join(attribute)
+
+        if isinstance(attribute, (tuple, list, collections.Iterable)):
+            attribute = [str(item) for item in attribute]
+            attribute = '.'.join(attribute)
+        else:
+            attribute = str(attribute)
 
         msg = ('Failed validating attribute "%s" in config for pack "%s" (%s): %s' %
                (attribute, pack_name, config_path, str(e)))


Test patch:
diff --git a/st2common/tests/unit/test_configs_registrar.py b/st2common/tests/unit/test_configs_registrar.py
--- a/st2common/tests/unit/test_configs_registrar.py
+++ b/st2common/tests/unit/test_configs_registrar.py
@@ -36,6 +36,7 @@
 
 PACK_1_PATH = os.path.join(fixturesloader.get_fixtures_packs_base_path(), 'dummy_pack_1')
 PACK_6_PATH = os.path.join(fixturesloader.get_fixtures_packs_base_path(), 'dummy_pack_6')
+PACK_19_PATH = os.path.join(fixturesloader.get_fixtures_packs_base_path(), 'dummy_pack_19')
 
 
 class ConfigsRegistrarTestCase(CleanDbTestCase):
@@ -89,7 +90,7 @@ def test_register_all_configs_invalid_config_no_config_schema(self):
         self.assertEqual(len(pack_dbs), 1)
         self.assertEqual(len(config_dbs), 1)
 
-    def test_register_all_configs_with_config_schema_validation_validation_failure(self):
+    def test_register_all_configs_with_config_schema_validation_validation_failure_1(self):
         # Verify DB is empty
         pack_dbs = Pack.get_all()
         config_dbs = Config.get_all()
@@ -117,3 +118,34 @@ def test_register_all_configs_with_config_schema_validation_validation_failure(s
         self.assertRaisesRegexp(ValueError, expected_msg,
                                 registrar.register_from_packs,
                                 base_dirs=packs_base_paths)
+
+    def test_register_all_configs_with_config_schema_validation_validation_failure_2(self):
+        # Verify DB is empty
+        pack_dbs = Pack.get_all()
+        config_dbs = Config.get_all()
+
+        self.assertEqual(len(pack_dbs), 0)
+        self.assertEqual(len(config_dbs), 0)
+
+        registrar = ConfigsRegistrar(use_pack_cache=False, fail_on_failure=True,
+                                     validate_configs=True)
+        registrar._pack_loader.get_packs = mock.Mock()
+        registrar._pack_loader.get_packs.return_value = {'dummy_pack_19': PACK_19_PATH}
+
+        # Register ConfigSchema for pack
+        registrar._register_pack_db = mock.Mock()
+        registrar._register_pack(pack_name='dummy_pack_19', pack_dir=PACK_19_PATH)
+        packs_base_paths = content_utils.get_packs_base_paths()
+
+        if six.PY3:
+            expected_msg = ('Failed validating attribute "instances.0.alias" in config for pack '
+                            '"dummy_pack_19" (.*?): {\'not\': \'string\'} is not of type '
+                            '\'string\'')
+        else:
+            expected_msg = ('Failed validating attribute "instances.0.alias" in config for pack '
+                            '"dummy_pack_19" (.*?): {\'not\': \'string\'} is not of type '
+                            'u\'string\'')
+
+        self.assertRaisesRegexp(ValueError, expected_msg,
+                                registrar.register_from_packs,
+                                base_dirs=packs_base_paths)