Repository: remind101/stacker
Pull request #169 (2016-07-13 23:57:01)

Support a configurable namespace delimiter
We're trying to adapt a lot of our existing troposphere stacks to stacker, and we've namespaced all of our stacks, but we've omitted the delimiter between the namespace and the stack name, in favor of simply camel casing our stack names. Some flexibility in the delimiter used there (including a blank string) would go a long way to providing some flexibility to start using stacker with stacks that might've been created by other methods.


Patch:
diff --git a/docs/config.rst b/docs/config.rst
index 71dd1e5..a0fe40f 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -13,13 +13,28 @@ duplicating config. (See `YAML anchors & references`_ for details)
 Top Level Keywords
 ==================
 
+Namespace Delimiter
+-------------------
+
+By default, stacker will use '-' as a delimiter between your namespace and the
+declared stack name to build the actual Cloudformation stack name that gets
+created. Since child resources of your stacks will, by default, use a portion
+of your stack name in the auto-generated resource names, the first characters
+of your fully-qualified stack name potentially convey valuable information to
+someone glancing at resource names. If you prefer to not use a delimiter, you
+can pass the **namespace_delimiter** top level key word in the config as an empty string.
+
+See the `Cloudformation API Reference`_ for allowed stack name characters
+
+.. _`Cloudformation API Reference`: http://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html
+
 S3 Bucket
 ---------
 
 Stacker, by default, pushes your Cloudformation templates into an S3 bucket
 and points Cloudformation at the template in that bucket when launching or
 updating your stacks. By default it uses a bucket named
-**stacker-${namespace}**, where the namespace is the namespace provied in the
+**stacker-${namespace}**, where the namespace is the namespace provided in the
 `environment <environments.html>`_ file.
 
 If you want to change this, provide the **stacker_bucket** top level key word
diff --git a/stacker/context.py b/stacker/context.py
index 6ba78f5..e1ca993 100644
--- a/stacker/context.py
+++ b/stacker/context.py
@@ -3,17 +3,17 @@ from .config import parse_config
 from .stack import Stack
 
 
-def get_fqn(base_fqn, name=None):
+def get_fqn(base_fqn, delimiter, name=None):
     """Return the fully qualified name of an object within this context.
 
     If the name passed already appears to be a fully qualified name, it
     will be returned with no further processing.
 
     """
-    if name and name.startswith(base_fqn + "-"):
+    if name and name.startswith("%s%s" % (base_fqn, delimiter)):
         return name
 
-    return "-".join(filter(None, [base_fqn, name]))
+    return delimiter.join(filter(None, [base_fqn, name]))
 
 
 class Context(object):
@@ -50,6 +50,7 @@ class Context(object):
         self.stack_names = stack_names or []
         self.parameters = parameters or {}
         self.mappings = mappings or {}
+        self.namespace_delimiter = "-"
         self.config = config or {}
         self.force_stacks = force_stacks or []
         self._base_fqn = self.namespace.replace(".", "-").lower()
@@ -58,6 +59,9 @@ class Context(object):
     def load_config(self, conf_string):
         self.config = parse_config(conf_string, environment=self.environment)
         self.mappings = self.config.get("mappings", {})
+        namespace_delimiter = self.config.get("namespace_delimiter", None)
+        if namespace_delimiter is not None:
+            self.namespace_delimiter = namespace_delimiter
         bucket_name = self.config.get("stacker_bucket", None)
         if bucket_name:
             self.bucket_name = bucket_name
@@ -105,4 +109,4 @@ class Context(object):
         will be returned with no further processing.
 
         """
-        return get_fqn(self._base_fqn, name)
+        return get_fqn(self._base_fqn, self.namespace_delimiter, name)


Test patch:
diff --git a/stacker/tests/test_context.py b/stacker/tests/test_context.py
index ed0a0b8..e0f2830 100644
--- a/stacker/tests/test_context.py
+++ b/stacker/tests/test_context.py
@@ -80,19 +80,36 @@ class TestContext(unittest.TestCase):
         context.load_config("""stacker_bucket: bucket123""")
         self.assertEqual(context.bucket_name, "bucket123")
 
+    def test_context_namespace_delimiter_is_overriden_and_not_none(self):
+        context = Context({"namespace": "namespace"})
+        context.load_config("""namespace_delimiter: '_'""")
+        fqn = context.get_fqn("stack1")
+        self.assertEqual(fqn, "namespace_stack1")
+
+    def test_context_namespace_delimiter_is_overriden_and_is_empty(self):
+        context = Context({"namespace": "namespace"})
+        context.load_config("""namespace_delimiter: ''""")
+        fqn = context.get_fqn("stack1")
+        self.assertEqual(fqn, "namespacestack1")
 
 class TestFunctions(unittest.TestCase):
     """ Test the module level functions """
     def test_get_fqn_redundant_base(self):
         base = "woot"
         name = "woot-blah"
-        self.assertEqual(get_fqn(base, name), name)
+        self.assertEqual(get_fqn(base, '-', name), name)
+        self.assertEqual(get_fqn(base, '', name), name)
+        self.assertEqual(get_fqn(base, '_', name), "woot_woot-blah")
 
     def test_get_fqn_only_base(self):
         base = "woot"
-        self.assertEqual(get_fqn(base), base)
+        self.assertEqual(get_fqn(base, '-'), base)
+        self.assertEqual(get_fqn(base, ''), base)
+        self.assertEqual(get_fqn(base, '_'), base)
 
     def test_get_fqn_full(self):
         base = "woot"
         name = "blah"
-        self.assertEqual(get_fqn(base, name), "%s-%s" % (base, name))
+        self.assertEqual(get_fqn(base, '-', name), "%s-%s" % (base, name))
+        self.assertEqual(get_fqn(base, '', name), "%s%s" % (base, name))
+        self.assertEqual(get_fqn(base, '_', name), "%s_%s" % (base, name))