Repository: RedHatInsights/insights-core

Issue:
config tree combiner incorrectly detects recursive includes
The http_conf combiner, when processing this:
```
<IfVersion < 2.4>
  Include /etc/httpd/conf.d/05-foreman.d/*.conf
</IfVersion>
<IfVersion >= 2.4>
  IncludeOptional /etc/httpd/conf.d/05-foreman.d/*.conf
</IfVersion>
```
generates the following:
```
File "/worktree/insights/insights-core/insights/core/__init__.py", line 144, in inner
    raise Exception(msg)
Exception: Configuration contains recursive includes: /etc/httpd/conf.d/05-foreman.d/*.conf
```
The [flatten code](https://github.com/RedHatInsights/insights-core/blob/master/insights/core/__init__.py#L133) hits the second include and assumes recursion since the files are identical to some it's already processed.


Discussion:

Resolving patch:
diff --git a/insights/core/__init__.py b/insights/core/__init__.py
--- a/insights/core/__init__.py
+++ b/insights/core/__init__.py
@@ -131,10 +131,14 @@ def find_main(confs, name):
 
 
 def flatten(docs, pred):
-    seen = set()
+    """
+    Replace include nodes with their config trees.  Allows the same files to be
+    included more than once so long as they don't induce a recursion.
+    """
     pred = compile_queries(pred)
 
-    def inner(children):
+    def inner(children, stack):
+        seen = set(stack)
         results = []
         for c in children:
             if pred([c]) and c.children:
@@ -142,14 +146,15 @@ def inner(children):
                 if name in seen:
                     msg = "Configuration contains recursive includes: %s" % name
                     raise Exception(msg)
-                seen.add(name)
-                results.extend(inner(c.children))
+                stack.append(name)
+                results.extend(inner(c.children, stack))
+                stack.pop()
             else:
                 results.append(c)
                 if c.children:
-                    c.children = inner(c.children)
+                    c.children = inner(c.children, stack)
         return results
-    return inner(docs)
+    return inner(docs, [])
 
 
 class ConfigComponent(object):


Test patch:
diff --git a/insights/combiners/tests/test_httpd_conf_tree.py b/insights/combiners/tests/test_httpd_conf_tree.py
--- a/insights/combiners/tests/test_httpd_conf_tree.py
+++ b/insights/combiners/tests/test_httpd_conf_tree.py
@@ -475,6 +475,16 @@
 """
 
 
+MULTIPLE_INCLUDES = """
+<IfVersion < 2.4>
+  Include /etc/httpd/conf.d/05-foreman.d/*.conf
+</IfVersion>
+<IfVersion >= 2.4>
+  IncludeOptional /etc/httpd/conf.d/05-foreman.d/*.conf
+</IfVersion>
+"""
+
+
 def test_mixed_case_tags():
     httpd = _HttpdConf(context_wrap(HTTPD_CONF_MIXED, path='/etc/httpd/conf/httpd.conf'))
     assert httpd.find("ServerLimit").value == 256
@@ -761,3 +771,17 @@ def test_unicode_comments():
 
     rewrite_cond = result["DNSSDEnable"]
     assert len(rewrite_cond) == 1
+
+
+def test_multiple_includes():
+    httpd1 = _HttpdConf(context_wrap(MULTIPLE_INCLUDES, path='/etc/httpd/conf/httpd.conf'))
+    httpd2 = _HttpdConf(context_wrap(UNICODE_COMMENTS, path='/etc/httpd/conf.d/05-foreman.d/hello.conf'))
+    result = HttpdConfTree([httpd1, httpd2])
+    assert len(result["IfVersion"]["DNSSDEnable"]) == 2
+
+
+def test_recursive_includes():
+    with pytest.raises(Exception):
+        httpd1 = _HttpdConf(context_wrap(MULTIPLE_INCLUDES, path='/etc/httpd/conf/httpd.conf'))
+        httpd2 = _HttpdConf(context_wrap(MULTIPLE_INCLUDES, path='/etc/httpd/conf.d/05-foreman.d/hello.conf'))
+        HttpdConfTree([httpd1, httpd2])