Repository: nginxinc/crossplane

Issue:
Option to ignore includes
This is a nice to have option at least in my usecase.   Right now I am 'text' validating the config generated by controller. Instead I plan to run in through crossplane and validate the args and the directives part. Controller has some default 'include' directives but does not generate them. So when I run it through crossplane it complains about those missing includes. 



Discussion:

Patch:
diff --git a/crossplane/__main__.py b/crossplane/__main__.py
index c92d7f3..16fe34a 100644
--- a/crossplane/__main__.py
+++ b/crossplane/__main__.py
@@ -5,6 +5,7 @@ import sys
 from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
 from traceback import format_exception
 
+from . import __version__
 from .lexer import lex as lex_file
 from .parser import parse as parse_file
 from .errors import NgxParserBaseException
@@ -70,12 +71,14 @@ def _dump_payload(obj, fp, indent):
     fp.write(json.dumps(obj, **kwargs) + '\n')
 
 
-def parse(filename, out, indent=None, catch=None, tb_onerror=None):
+def parse(filename, out, indent=None, catch=None, tb_onerror=None, ignore='', single=False):
+    ignore = ignore.split(',') if ignore else []
+
     def callback(e):
         exc = sys.exc_info() + (10,)
         return ''.join(format_exception(*exc)).rstrip()
 
-    kwargs = {'catch_errors': catch}
+    kwargs = {'catch_errors': catch, 'ignore': ignore, 'single': single}
     if tb_onerror:
         kwargs['onerror'] = callback
 
@@ -157,6 +160,7 @@ def parse_args(args=None):
         description='various operations for nginx config files',
         usage='%(prog)s <command> [options]'
     )
+    parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
     subparsers = parser.add_subparsers(title='commands')
 
     def create_subparser(function, help):
@@ -170,8 +174,10 @@ def parse_args(args=None):
     p.add_argument('filename', help='the nginx config file')
     p.add_argument('-o', '--out', type=FileType('w'), default='-', help='write output to a file')
     p.add_argument('-i', '--indent', type=int, metavar='NUM', help='number of spaces to indent output')
+    p.add_argument('--ignore', metavar='DIRECTIVES', default='', help='ignore directives (comma-separated)')
     p.add_argument('--no-catch', action='store_false', dest='catch', help='only collect first error in file')
     p.add_argument('--tb-onerror', action='store_true', help='include tracebacks in config errors')
+    p.add_argument('--single-file', action='store_true', dest='single', help='do not include other config files')
 
     p = create_subparser(lex, 'lexes tokens from an nginx config file')
     p.add_argument('filename', help='the nginx config file')
@@ -201,7 +207,7 @@ def parse_args(args=None):
 
     parsed = parser.parse_args(args=args)
 
-    # this addresses a bug  that was added to argparse in Python 3.3
+    # this addresses a bug that was added to argparse in Python 3.3
     if not parsed.__dict__:
         parser.error('too few arguments')
 
diff --git a/crossplane/parser.py b/crossplane/parser.py
index 8f0e0f0..d91d835 100644
--- a/crossplane/parser.py
+++ b/crossplane/parser.py
@@ -19,8 +19,17 @@ def _prepare_if_args(stmt):
         args[:] = args[start:end]
 
 
-def parse(filename, onerror=None, catch_errors=True):
-    """Parses an nginx config file and returns a nested dict payload"""
+def parse(filename, onerror=None, catch_errors=True, ignore=(), single=False):
+    """
+    Parses an nginx config file and returns a nested dict payload
+    
+    :param filename: string contianing the name of the config file to parse
+    :param onerror: function that determines what's saved in "callback"
+    :param catch_errors: bool; if False, parse stops after first error
+    :param ignore: list or tuple of directives to exclude from the payload
+    :param single: bool; if True, including from other files doesn't happen
+    :returns: a payload that describes the parsed nginx config
+    """
     config_dir = os.path.dirname(filename)
 
     payload = {
@@ -82,6 +91,13 @@ def parse(filename, onerror=None, catch_errors=True):
                 stmt['args'].append(token)
                 token, __ = next(tokens)
 
+            # consume the directive if it is ignored and move on
+            if stmt['directive'] in ignore:
+                # if this directive was a block consume it too
+                if token == '{':
+                    _parse(parsing, tokens, consume=True)
+                continue
+
             # prepare arguments
             if stmt['directive'] == 'if':
                 _prepare_if_args(stmt)
@@ -103,7 +119,7 @@ def parse(filename, onerror=None, catch_errors=True):
                     raise e
 
             # add "includes" to the payload if this is an include statement
-            if stmt['directive'] == 'include':
+            if not single and stmt['directive'] == 'include':
                 pattern = args[0]
                 if not os.path.isabs(args[0]):
                     pattern = os.path.join(config_dir, args[0])


Test patch:
diff --git a/tests/test_parse.py b/tests/test_parse.py
index da5fe2b..c407ee9 100644
--- a/tests/test_parse.py
+++ b/tests/test_parse.py
@@ -247,3 +247,123 @@ def test_includes_globbed():
             }
         ]
     }
+
+
+def test_includes_single():
+    dirname = os.path.join(here, 'configs', 'includes-regular')
+    config = os.path.join(dirname, 'nginx.conf')
+    payload = crossplane.parse(config, single=True)
+    assert payload == {
+        'status': 'ok',
+        'errors': [],
+        'config': [
+            {
+                'file': os.path.join(dirname, 'nginx.conf'),
+                'status': 'ok',
+                'errors': [],
+                'parsed': [
+                    {
+                        'directive': 'events',
+                        'line': 1,
+                        'args': [],
+                        'block': []
+                    },
+                    {
+                        'directive': 'http',
+                        'line': 2,
+                        'args': [],
+                        'block': [
+                            {
+                                'directive': 'include',
+                                'line': 3,
+                                'args': ['conf.d/server.conf']
+                                # no 'includes' key
+                            }
+                        ]
+                    }
+                ]
+            }
+            # single config parsed
+        ]
+    }
+
+
+def test_ignore_directives():
+    dirname = os.path.join(here, 'configs', 'simple')
+    config = os.path.join(dirname, 'nginx.conf')
+
+    # check that you can ignore multiple directives
+    payload = crossplane.parse(config, ignore=['listen', 'server_name'])
+    assert payload == {
+        "status": "ok",
+        "errors": [],
+        "config": [
+            {
+                "file": os.path.join(dirname, 'nginx.conf'),
+                "status": "ok",
+                "errors": [],
+                "parsed": [
+                    {
+                        "directive": "events",
+                        "line": 1,
+                        "args": [],
+                        "block": [
+                            {
+                                "directive": "worker_connections",
+                                "line": 2,
+                                "args": ["1024"]
+                            }
+                        ]
+                    },
+                    {
+                        "directive": "http",
+                        "line": 5,
+                        "args": [],
+                        "block": [
+                            {
+                                "directive": "server",
+                                "line": 6,
+                                "args": [],
+                                "block": [
+                                    {
+                                        "directive": "location",
+                                        "line": 9,
+                                        "args": ["/"],
+                                        "block": [
+                                            {
+                                                "directive": "return",
+                                                "line": 10,
+                                                "args": ["200", "foo bar baz"]
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                ]
+            }
+        ]
+    }
+
+    # check that you can also ignore block directives
+    payload = crossplane.parse(config, ignore=['events', 'server'])
+    assert payload == {
+        "status": "ok",
+        "errors": [],
+        "config": [
+            {
+                "file": os.path.join(dirname, 'nginx.conf'),
+                "status": "ok",
+                "errors": [],
+                "parsed": [
+                    {
+                        "directive": "http",
+                        "line": 5,
+                        "args": [],
+                        "block": []
+                    }
+                ]
+            }
+        ]
+    }