Repository: 20c/ctl

Issue:
Better error handling for config errors outside of `plugins`
Example: having a schema error in `permissions` exits ctl with traceback that's not very telling as to what is failing

reproduce:

```
permissions:
  namespace: ctl
  permission: crud
```

Discussion:

Patch:
diff --git a/Ctl/Pipfile b/Ctl/Pipfile
index 0c7a304..1bd6308 100644
--- a/Ctl/Pipfile
+++ b/Ctl/Pipfile
@@ -14,7 +14,7 @@ tmpl = "==0.3.0"
 
 [packages]
 munge = "<1,>=0.4"
-cfu = ">=1.2.0,<2"
+cfu = ">=1.3.0,<2"
 grainy = ">=1.4.0,<2"
 git-url-parse = ">=1.1.0,<2"
 pluginmgr = ">=0.6"
diff --git a/Ctl/requirements.txt b/Ctl/requirements.txt
index b3582c5..0037aaa 100644
--- a/Ctl/requirements.txt
+++ b/Ctl/requirements.txt
@@ -1,5 +1,5 @@
 munge >=0.4, <1
-cfu >= 1.2.0, < 2
+cfu >= 1.3.0, < 2
 grainy >= 1.4.0, <2
 git-url-parse >= 1.1.0, <2
 pluginmgr >= 0.6
diff --git a/src/ctl/__init__.py b/src/ctl/__init__.py
index eb4a635..b9616df 100644
--- a/src/ctl/__init__.py
+++ b/src/ctl/__init__.py
@@ -4,6 +4,7 @@ import os
 from pkg_resources import get_distribution
 
 import confu.config
+import confu.exceptions
 import grainy.core
 import copy
 import logging
@@ -279,11 +280,14 @@ class Ctl(object):
     # def set_config_dir(self):
 
     def __init__(self, ctx=None, config_dir=None, full_init=True):
-        self.init_context(ctx=ctx, config_dir=config_dir)
 
+        self.init_context(ctx=ctx, config_dir=config_dir)
         self.init_logging()
-        self.init_permissions()
 
+        if self.config.errors:
+            return self.log_config_issues()
+
+        self.init_permissions()
         self.expose_plugin_vars()
 
         if full_init:
@@ -330,8 +334,10 @@ class Ctl(object):
         Apply python logging config and create `log` and `usage_log`
         properties
         """
+
         # allow setting up python logging from ctl config
         set_pylogger_config(self.ctx.config.get_nested("ctl", "log"))
+
         # instantiate logger
         self.log = Log("ctl")
         self.usage_log = Log("usage")
diff --git a/src/ctl/plugins/tmp b/src/ctl/plugins/tmp
new file mode 100644
index 0000000..41bd689
--- /dev/null
+++ b/src/ctl/plugins/tmp
@@ -0,0 +1,57 @@
+plugins:
+
+  # plugin that controls the repo we want to install from
+  - type: git
+    name: git_fullctl
+    config:
+      repo_url: git@git.20c.com:fullctl/www.fullctl.com
+
+
+  # copy files 
+  - type: copy 
+    name: copy_fullctl
+    config:
+      src: {{tmp}}/git@git.20c.com:fullctl/www.fullctl.com
+      dest: {{tmp}}/builds/www.fullctl.com/{{tag}}
+      walk_dirs:
+        - {{src}}/fullctl/website
+        - {{src}}/fullctl/ixportal
+
+
+  # template files
+  - type: template
+    name: template_fullctl
+    config:
+      src: {{tmp}}/git@git.20c.com:fullctl/www.fullctl.com
+      dest: {{tmp}}/builds/www.fullctl.com/{{tag}}/fullctl
+      walk_dirs:
+        - {{src}}Ctl/fullctl/templates
+      vars:
+        - {{src}}/Ctl/fullctl/{{env}}.yaml  
+          
+
+  # rsync
+  - type: rsync
+    name: rsync_fullctl
+    config:
+      gateways:
+        beta:
+          - host: 127.0.0.1
+            user: fullctl
+
+  # run installation sequence
+  - type: chain
+    name: facsimile
+    config:
+      chains:
+        beta:
+          # stage `git` - checks out the tag passed in the cli
+          - git_fullctl checkout {{tag}}
+          # stage `copy` - copy project files
+          - copy_fullctl beta {{tag}}
+          # stage `template` - build template files
+          - template_fullctl beta {{tag}}
+          # stage `rsync`
+          - rsync_fullctl beta {{tmp}}/builds/www.fullctl.com/{{tag}}
+
+
diff --git a/src/ctl/util/versioning.py b/src/ctl/util/versioning.py
index 22bdb09..23e1390 100644
--- a/src/ctl/util/versioning.py
+++ b/src/ctl/util/versioning.py
@@ -1,5 +1,4 @@
 def version_tuple(version):
-    print("VERSION", version)
     """ Returns a tuple from version string """
     return tuple(version.split("."))
 
@@ -9,27 +8,35 @@ def version_string(version):
     return ".".join(["{}".format(v) for v in version])
 
 
-def validate_semantic(version):
+def validate_semantic(version, pad=0):
     if not isinstance(version, (list, tuple)):
         version = version_tuple(version)
 
-    try:
-        major, minor, patch, dev = version
-    except ValueError:
-        major, minor, patch = version
+    parts = len(version)
+
+    if parts < 1:
+        raise ValueError("Semantic version needs to contain at least a major version")
+    if parts > 4:
+        raise ValueError("Semantic version can not contain more than 4 parts")
+
+    if parts < pad:
+        version = tuple(list(version) + [0 for i in range(0, pad - parts)])
 
     return tuple([int(n) for n in version])
 
 
 def bump_semantic(version, segment):
-    version = list(validate_semantic(version))
     if segment == "major":
+        version = list(validate_semantic(version))
         return (version[0] + 1, 0, 0)
     elif segment == "minor":
+        version = list(validate_semantic(version, pad=2))
         return (version[0], version[1] + 1, 0)
     elif segment == "patch":
+        version = list(validate_semantic(version, pad=3))
         return (version[0], version[1], version[2] + 1)
     elif segment == "dev":
+        version = list(validate_semantic(version, pad=4))
         try:
             return (version[0], version[1], version[2], version[3] + 1)
         except IndexError:


Test patch:
diff --git a/tests/test_plugin_version.py b/tests/test_plugin_version.py
index 6745c78..4b9617a 100644
--- a/tests/test_plugin_version.py
+++ b/tests/test_plugin_version.py
@@ -138,6 +138,30 @@ def test_bump(tmpdir, ctlr):
         plugin.bump(version="invalid", repo="dummy_repo")
 
 
+def test_bump_truncated(tmpdir, ctlr):
+    plugin, dummy_repo = instantiate(tmpdir, ctlr)
+    plugin.tag(version="1.0", repo="dummy_repo")
+
+    plugin.bump(version="minor", repo="dummy_repo")
+    assert dummy_repo.version == ("1", "1", "0")
+    assert dummy_repo._tag == "1.1.0"
+
+    plugin.tag(version="1.0", repo="dummy_repo")
+    plugin.bump(version="patch", repo="dummy_repo")
+    assert dummy_repo.version == ("1", "0", "1")
+    assert dummy_repo._tag == "1.0.1"
+
+    plugin.tag(version="2", repo="dummy_repo")
+    plugin.bump(version="patch", repo="dummy_repo")
+    assert dummy_repo.version == ("2", "0", "1")
+    assert dummy_repo._tag == "2.0.1"
+
+    plugin.tag(version="3", repo="dummy_repo")
+    plugin.bump(version="major", repo="dummy_repo")
+    assert dummy_repo.version == ("4", "0", "0")
+    assert dummy_repo._tag == "4.0.0"
+
+
 def test_execute(tmpdir, ctlr):
     plugin, dummy_repo = instantiate(tmpdir, ctlr)
     plugin.execute(op="tag", version="1.0.0", repository="dummy_repo", init=True)
diff --git a/tests/test_util_versioning.py b/tests/test_util_versioning.py
index b89df79..6624816 100644
--- a/tests/test_util_versioning.py
+++ b/tests/test_util_versioning.py
@@ -19,7 +19,7 @@ def test_version_tuple(version, string):
         ((1, 0, 0), (1, 0, 0), None),
         ("1.0.0.0", (1, 0, 0, 0), None),
         ((1, 0, 0, 0), (1, 0, 0, 0), None),
-        ("1.0", None, ValueError),
+        ("1.0", (1, 0), None),
         ("a.b.c", None, ValueError),
     ],
 )