Repository: deis/deis

Issue:
config:set error after brand new example-go pushed
I installed a example-go to my new deis (also installed a few days ago, version 75ba179)
Then after the `git push deis master`, I tried a deis config:set POWERED_BY="Ological" and it told me `Creating config... 500 INTERNAL SERVER ERROR
<h1>Server Error (500)</h1>` right in the face !
I hated it and therefore I thought I should open an issue about it

Here is the deis-controller log for that

```
2014-09-03 11:43:06 [154] [DEBUG] POST /api/apps
INFO rubber-jamboree: config rubber-jamboree-29140d7 updated
INFO rubber-jamboree: build rubber-jamboree-fa74c46 created
INFO rubber-jamboree: release rubber-jamboree-v1 created
2014-09-03 11:43:15 [154] [DEBUG] POST /api/hooks/push
2014-09-03 11:43:15 [154] [DEBUG] POST /api/hooks/config
2014-09-03 11:43:51 [151] [DEBUG] POST /api/hooks/build
INFO rubber-jamboree: build rubber-jamboree-e0cdfaa created
INFO rubber-jamboree: release rubber-jamboree-v2 created
[2014-09-03 11:43:51,563: INFO/MainProcess] Received task: api.tasks.deploy_release[d9e79daf-a917-4fdc-b99d-f384834cd7e5]
[2014-09-03 11:43:51,584: INFO/MainProcess] Task api.tasks.deploy_release[d9e79daf-a917-4fdc-b99d-f384834cd7e5] succeeded in 0.0187781249997s: None
[2014-09-03 11:43:52,118: INFO/MainProcess] Received task: api.tasks.start_containers[202f7aa8-70af-47c8-9d0a-348e06d2c459]
[2014-09-03 11:43:52,160: WARNING/Worker-8] Creating rubber-jamboree_v2.web.1
[2014-09-03 11:43:53,098: WARNING/Worker-8] Starting rubber-jamboree_v2.web.1
Triggered job rubber-jamboree_v2.web.1.service start
Triggered job rubber-jamboree_v2.web.1-log.service start
Triggered job rubber-jamboree_v2.web.1-announce.service start
[2014-09-03 11:44:13,878: INFO/MainProcess] Task api.tasks.start_containers[202f7aa8-70af-47c8-9d0a-348e06d2c459] succeeded in 21.753724118s: None
INFO rubber-jamboree: containers scaled web=1
2014-09-03 11:44:26 [163] [DEBUG] GET /api/apps/rubber-jamboree/config
2014-09-03 11:44:36 [152] [DEBUG] POST /api/apps/rubber-jamboree/config
INFO rubber-jamboree: config rubber-jamboree-f81d1cb updated
INFO rubber-jamboree: release rubber-jamboree-v3 created
ERROR Internal Server Error: /api/apps/rubber-jamboree/config
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 112, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/viewsets.py", line 78, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/django/views/decorators/csrf.py", line 57, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/views.py", line 400, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/views.py", line 397, in dispatch
    response = handler(request, *args, **kwargs)
  File "/app/api/views.py", line 414, in create
    return super(AppConfigViewSet, self).create(request, *args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/mixins.py", line 55, in create
    self.post_save(self.object, created=True)
  File "/app/api/views.py", line 387, in post_save
    self.release = release.new(self.request.user, config=config)
  File "/app/api/models.py", line 554, in new
    release_image,)
  File "/app/registry/private.py", line 56, in publish_release
    image['config']['Env'] = _construct_env(image['config']['Env'], config)
  File "/app/registry/private.py", line 172, in _construct_env
    new_env.append("{}={}".format(k, v))
UnicodeEncodeError: 'ascii' codec can't encode character u'\xa0' in position 4: ordinal not in range(128)
```



Discussion:
Apparently `deis config:set` can't handle Unicode:

``` console
$ deis config:set POWERED_BY=Geniuses
Creating config... done, v3

=== nimbus-undertow
POWERED_BY: Geniuses
$ deis config:set POWERED_BY=Дураки
Creating config... 500 INTERNAL SERVER ERROR
<h1>Server Error (500)</h1>
```

Indeed it seems. I thought I used a normal space character, but it was a non-breaking space. Should I let this issue opened ? Or should we create a better named issue ?

We can leave this issue open and change the title to reflect that it's a general unicode issue. I have a possible fix for it I'm testing now.


Resolving patch:
diff --git a/client/deis.py b/client/deis.py
--- a/client/deis.py
+++ b/client/deis.py
@@ -1112,11 +1112,13 @@ def config_list(self, args):
                 width = max(map(len, keys)) + 5
                 for k in keys:
                     v = values[k]
+                    k, v = k.encode('utf-8'), v.encode('utf-8')
                     self._logger.info(("{k:<" + str(width) + "} {v}").format(**locals()))
             else:
                 output = []
                 for k in keys:
                     v = values[k]
+                    k, v = k.encode('utf-8'), v.encode('utf-8')
                     output.append("{k}={v}".format(**locals()))
                 self._logger.info(' '.join(output))
         else:
@@ -1162,6 +1164,7 @@ def config_set(self, args):
                 self._logger.info('No configuration')
                 return
             for k, v in values.items():
+                k, v = k.encode('utf-8'), v.encode('utf-8')
                 self._logger.info("{k}: {v}".format(**locals()))
         else:
             raise ResponseError(response)
diff --git a/controller/api/models.py b/controller/api/models.py
--- a/controller/api/models.py
+++ b/controller/api/models.py
@@ -172,9 +172,8 @@ def log(self, message):
         existing logging configurations.
         """
         with open(os.path.join(settings.DEIS_LOG_DIR, self.id + '.log'), 'a') as f:
-            f.write('{} deis[api]: {}\n'.format(
-                time.strftime('%Y-%m-%d %H:%M:%S'),
-                message))
+            msg = "{} deis[api]: {}\n".format(time.strftime('%Y-%m-%d %H:%M:%S'), message)
+            f.write(msg.encode('utf-8'))
 
     def create(self, *args, **kwargs):
         config = Config.objects.create(owner=self.owner, app=self)
diff --git a/controller/registry/private.py b/controller/registry/private.py
--- a/controller/registry/private.py
+++ b/controller/registry/private.py
@@ -166,10 +166,10 @@ def _construct_env(env, config):
         if k in config:
             # update values defined by config
             v = config.pop(k)
-        new_env.append("{}={}".format(k, v))
+        new_env.append("{}={}".format(k.encode('utf-8'), v.encode('utf-8')))
     # add other config ENV items
     for k, v in config.items():
-        new_env.append("{}={}".format(k, v))
+        new_env.append("{}={}".format(k.encode('utf-8'), v.encode('utf-8')))
     return new_env
 
 


Test patch:
diff --git a/controller/api/tests/test_config.py b/controller/api/tests/test_config.py
--- a/controller/api/tests/test_config.py
+++ b/controller/api/tests/test_config.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 """
 Unit tests for the Deis api app.
 
@@ -127,6 +128,29 @@ def test_config_set_same_key(self):
         self.assertIn('PORT', json.loads(response.data['values']))
         self.assertEqual(json.loads(response.data['values'])['PORT'], '5001')
 
+    @mock.patch('requests.post', mock_import_repository_task)
+    def test_config_set_unicode(self):
+        """
+        Test that config sets with unicode values are accepted.
+        """
+        url = '/api/apps'
+        body = {'cluster': 'autotest'}
+        response = self.client.post(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 201)
+        app_id = response.data['id']
+        url = "/api/apps/{app_id}/config".format(**locals())
+        # set an initial config value
+        body = {'values': json.dumps({'POWERED_BY': 'Деис'})}
+        response = self.client.post(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 201)
+        self.assertIn('POWERED_BY', json.loads(response.data['values']))
+        # reset same config value
+        body = {'values': json.dumps({'POWERED_BY': 'Кроликов'})}
+        response = self.client.post(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 201)
+        self.assertIn('POWERED_BY', json.loads(response.data['values']))
+        self.assertEqual(json.loads(response.data['values'])['POWERED_BY'], 'Кроликов')
+
     @mock.patch('requests.post', mock_import_repository_task)
     def test_config_str(self):
         """Test the text representation of a node."""
diff --git a/tests/config_test.go b/tests/config_test.go
--- a/tests/config_test.go
+++ b/tests/config_test.go
@@ -10,8 +10,8 @@ import (
 
 var (
 	configListCmd  = "config:list --app={{.AppName}}"
-	configSetCmd   = "config:set jaf=1 --app={{.AppName}}"
-	configUnsetCmd = "config:unset jaf --app={{.AppName}}"
+	configSetCmd   = "config:set FOO=讲台 --app={{.AppName}}"
+	configUnsetCmd = "config:unset FOO --app={{.AppName}}"
 )
 
 func TestConfig(t *testing.T) {
@@ -48,7 +48,7 @@ func configSetup(t *testing.T) *utils.DeisTestConfig {
 
 func configListTest(
 	t *testing.T, params *utils.DeisTestConfig, notflag bool) {
-	utils.CheckList(t, configListCmd, params, "jaf", notflag)
+	utils.CheckList(t, configListCmd, params, "FOO", notflag)
 }
 
 func configSetTest(t *testing.T, params *utils.DeisTestConfig) {