Repository: cloudtools/stacker

Issue:
file lookup returning <type 'unicode'> where it previously returned <class 'troposphere.AWSHelperFn'>
As of 1.4.0, the use of the `${file parameterized }` lookup no longer works with blueprints using variable type of `troposphere.AWSHelperFn`. This was working in previous versions - most recently 1.3.0.

### Error

```
File "/usr/local/lib/python2.7/site-packages/stacker/plan.py", line 93, in _run_once
  status = self.fn(self.stack, status=self.status)
File "/usr/local/lib/python2.7/site-packages/stacker/actions/build.py", line 321, in _launch_stack
  stack.resolve(self.context, self.provider)
File "/usr/local/lib/python2.7/site-packages/stacker/stack.py", line 196, in resolve
  self.blueprint.resolve_variables(self.variables)
File "/usr/local/lib/python2.7/site-packages/stacker/blueprints/base.py", line 452, in resolve_variables
  self.name
File "/usr/local/lib/python2.7/site-packages/stacker/blueprints/base.py", line 226, in resolve_variable
  value = validate_variable_type(var_name, var_type, value)
File "/usr/local/lib/python2.7/site-packages/stacker/blueprints/base.py", line 147, in validate_variable_type
  "type: %s." % (var_name, var_type, type(value))
ValueError: Value for variable ExampleParameter must be of type <class 'troposphere.AWSHelperFn'>. Actual type: <type 'unicode'>.
```

### System Information

**Operating System:** Mac OS X 10.13.6 build 17G65
**Python Version:** 2.7.14
**Stacker Version:** 1.4.0

### Files

```
├── top-level-folder
│   ├── blueprints
│   │   ├── __init__.py
│   │   └── example_blueprint.py
│   ├── file-to-reference.json
│   ├── example.env
│   └── stacker-config.yaml
```

#### stacker-config.yaml

```
namespace: example
stacker_bucket: ""

sys_path: ./

stacks:
  example-stack:
    class_path: blueprints.example_blueprint.BlueprintClass
    enabled: true
    variables:
      ExampleParameter: ${file parameterized:file://file-to-reference.json}
```

#### blueprints/example_blueprint.py

```python
from troposphere import AWSHelperFn
from stacker.blueprints.base import Blueprint

class BlueprintClass(Blueprint):

    VARIABLES = {
        'ExampleParameter': {
            'type': AWSHelperFn
        }
    }
```


Discussion:

Patch:
diff --git a/stacker/lookups/handlers/file.py b/stacker/lookups/handlers/file.py
index 2ea8893..a57af66 100644
--- a/stacker/lookups/handlers/file.py
+++ b/stacker/lookups/handlers/file.py
@@ -136,7 +136,7 @@ def _parameterize_string(raw):
         s_index = match.end()
 
     if not parts:
-        return raw
+        return GenericHelperFn(raw)
 
     parts.append(raw[s_index:])
     return GenericHelperFn({u"Fn::Join": [u"", parts]})
@@ -152,7 +152,7 @@ def parameterized_codec(raw, b64):
             call
 
     Returns:
-        :class:`troposphere.GenericHelperFn`: output to be included in a
+        :class:`troposphere.AWSHelperFn`: output to be included in a
         CloudFormation template.
     """
 
diff --git a/stacker/providers/aws/default.py b/stacker/providers/aws/default.py
index 1c47ecd..369d0b9 100644
--- a/stacker/providers/aws/default.py
+++ b/stacker/providers/aws/default.py
@@ -825,9 +825,17 @@ class Provider(BaseProvider):
             self.cloudformation, fqn, template, parameters, tags,
             'UPDATE', service_role=self.service_role, **kwargs
         )
+        old_parameters_as_dict = self.params_as_dict(old_parameters)
+        new_parameters_as_dict = self.params_as_dict(
+            [x
+             if x.get('ParameterValue')
+             else {'ParameterKey': x['ParameterKey'],
+                   'ParameterValue': old_parameters_as_dict[x['ParameterKey']]}
+             for x in parameters]
+        )
         params_diff = diff_parameters(
-            self.params_as_dict(old_parameters),
-            self.params_as_dict(parameters))
+            old_parameters_as_dict,
+            new_parameters_as_dict)
 
         action = "replacements" if self.replacements_only else "changes"
         full_changeset = changes


Test patch:
diff --git a/stacker/tests/lookups/handlers/test_file.py b/stacker/tests/lookups/handlers/test_file.py
index c2eb93f..312f71a 100644
--- a/stacker/tests/lookups/handlers/test_file.py
+++ b/stacker/tests/lookups/handlers/test_file.py
@@ -9,7 +9,7 @@ import mock
 import base64
 import yaml
 import json
-from troposphere import Base64, Join
+from troposphere import Base64, GenericHelperFn, Join
 
 from stacker.lookups.handlers.file import (json_codec, handler,
                                            parameterized_codec, yaml_codec)
@@ -46,12 +46,21 @@ class TestFileTranslator(unittest.TestCase):
         )
 
         out = parameterized_codec(u'Test {{Interpolation}} Here', True)
+        self.assertEqual(Base64, out.__class__)
         self.assertTemplateEqual(expected, out)
 
     def test_parameterized_codec_plain(self):
         expected = Join(u'', [u'Test ', {u'Ref': u'Interpolation'}, u' Here'])
 
         out = parameterized_codec(u'Test {{Interpolation}} Here', False)
+        self.assertEqual(GenericHelperFn, out.__class__)
+        self.assertTemplateEqual(expected, out)
+
+    def test_parameterized_codec_plain_no_interpolation(self):
+        expected = u'Test Without Interpolation Here'
+
+        out = parameterized_codec(u'Test Without Interpolation Here', False)
+        self.assertEqual(GenericHelperFn, out.__class__)
         self.assertTemplateEqual(expected, out)
 
     def test_yaml_codec_raw(self):