Repository: deis/deis

Issue:
Setting of config values prior to initial build results in an error
On a brand new setup (controller, chef-server) I get the following when trying to set configs on an application which hasn't yet been deployed:

**deis client:**

```
$deis config:set ASSET_HOST=http://assets.xxxxxxxxxxx --app=xxxxxxxx
500 INTERNAL SERVER ERROR
<h1>Server Error (500)</h1>
```

**deis/server:v0.1.0:**

```
2014-02-19 09:37:15 [132] [DEBUG] POST /api/apps/xxxxxxxxxxxx/config
INFO xxxxxxxxxxxx: Config xxxxxxxxxxxx-v4 updated
INFO xxxxxxxxxxxx: Release xxxxxxxxxxxx-v4 created
ERROR Internal Server Error: /api/apps/xxxxxxxxxxxx/config
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 114, 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 399, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/views.py", line 396, in dispatch
    response = handler(request, *args, **kwargs)
  File "/app/deis/api/views.py", line 586, 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/deis/api/views.py", line 568, in post_save
    user=self.request.user)
  File "/usr/local/lib/python2.7/dist-packages/django/dispatch/dispatcher.py", line 185, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/app/deis/api/models.py", line 867, in new_release
    publish_release(repository_path, config.values, tag)
  File "/app/deis/api/docker.py", line 23, in publish_release
    image_id = _get_tag(repository_path, 'latest')
  File "/app/deis/api/docker.py", line 44, in _get_tag
    raise RuntimeError("GET Image Error ({}: {})".format(r.status_code, r.text))
RuntimeError: GET Image Error (404: {
    "error": "Tag not found"
})
```

However it appears to set the value:

```
$deis config:list --app=xxxxxxxxxxxx
== xxxxxxxxxxxx Config
BOB: BILL
ASSET_HOST: xxxxxxxxxxxx
```

Setting of config values prior to initial build results in an error
On a brand new setup (controller, chef-server) I get the following when trying to set configs on an application which hasn't yet been deployed:

**deis client:**

```
$deis config:set ASSET_HOST=http://assets.xxxxxxxxxxx --app=xxxxxxxx
500 INTERNAL SERVER ERROR
<h1>Server Error (500)</h1>
```

**deis/server:v0.1.0:**

```
2014-02-19 09:37:15 [132] [DEBUG] POST /api/apps/xxxxxxxxxxxx/config
INFO xxxxxxxxxxxx: Config xxxxxxxxxxxx-v4 updated
INFO xxxxxxxxxxxx: Release xxxxxxxxxxxx-v4 created
ERROR Internal Server Error: /api/apps/xxxxxxxxxxxx/config
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 114, 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 399, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/views.py", line 396, in dispatch
    response = handler(request, *args, **kwargs)
  File "/app/deis/api/views.py", line 586, 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/deis/api/views.py", line 568, in post_save
    user=self.request.user)
  File "/usr/local/lib/python2.7/dist-packages/django/dispatch/dispatcher.py", line 185, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/app/deis/api/models.py", line 867, in new_release
    publish_release(repository_path, config.values, tag)
  File "/app/deis/api/docker.py", line 23, in publish_release
    image_id = _get_tag(repository_path, 'latest')
  File "/app/deis/api/docker.py", line 44, in _get_tag
    raise RuntimeError("GET Image Error ({}: {})".format(r.status_code, r.text))
RuntimeError: GET Image Error (404: {
    "error": "Tag not found"
})
```

However it appears to set the value:

```
$deis config:list --app=xxxxxxxxxxxx
== xxxxxxxxxxxx Config
BOB: BILL
ASSET_HOST: xxxxxxxxxxxx
```



Discussion:
This also just happened to me. BTW, any chance that the same approach used on #379 can be used to load those variables before build? 

The problem here is that config updates are designed to result in a new Docker Image layer.  In this case we have no source Docker image.  I imagine we can fix this by skipping the layer creation if the build does not yet exist.

@gabrtv the layer has not yet been pushed at this time. It's trying to retrieve the repository's `latest` tag so the environment variables can be set on top of the parent's image _before_ the layer is created.

Maybe if no tag exists (i.e. `_get_tag` throws a `RuntimeError`), let's push a bare image with just an `id` and an empty string for a `parent`, push an empty layer, tag it `v0` and call it a day. `v0` would be considered parallel to creating a repository on Github, but you haven't pushed any source code to it. Savvy?

@bacongobbler I don't think we should be pushing any metadata layers to the registry before we have a true base layer (from a build).  What's the best way to make this a no-op?

> What's the best way to make this a no-op?

Well, the idea is that users should be able to set config vars before initial build, since Heroku provides that. In order to do that, we need to store metadata _somewhere_. We could either throw the metadata into the `Release` model or introduce metadata layers in the registry before the initial build. I'm leaning more towards the latter, as the registry can already handle that use case quite well, and it reduces coupling between the registry and our API server for releases.

@bacongobbler ok, let's try adding the metadata layer for initial config as you suggested.  Given  `v1` is generated on app create, it sounds like initial config should result in `v2` on the registry.  First build would then be `v3`.  See `deis releases` for details.

This also just happened to me. BTW, any chance that the same approach used on #379 can be used to load those variables before build? 

The problem here is that config updates are designed to result in a new Docker Image layer.  In this case we have no source Docker image.  I imagine we can fix this by skipping the layer creation if the build does not yet exist.

@gabrtv the layer has not yet been pushed at this time. It's trying to retrieve the repository's `latest` tag so the environment variables can be set on top of the parent's image _before_ the layer is created.

Maybe if no tag exists (i.e. `_get_tag` throws a `RuntimeError`), let's push a bare image with just an `id` and an empty string for a `parent`, push an empty layer, tag it `v0` and call it a day. `v0` would be considered parallel to creating a repository on Github, but you haven't pushed any source code to it. Savvy?

@bacongobbler I don't think we should be pushing any metadata layers to the registry before we have a true base layer (from a build).  What's the best way to make this a no-op?

> What's the best way to make this a no-op?

Well, the idea is that users should be able to set config vars before initial build, since Heroku provides that. In order to do that, we need to store metadata _somewhere_. We could either throw the metadata into the `Release` model or introduce metadata layers in the registry before the initial build. I'm leaning more towards the latter, as the registry can already handle that use case quite well, and it reduces coupling between the registry and our API server for releases.

@bacongobbler ok, let's try adding the metadata layer for initial config as you suggested.  Given  `v1` is generated on app create, it sounds like initial config should result in `v2` on the registry.  First build would then be `v3`.  See `deis releases` for details.


Resolving patch:
diff --git a/api/docker.py b/api/docker.py
--- a/api/docker.py
+++ b/api/docker.py
@@ -20,22 +20,45 @@ def publish_release(repository_path, config, tag):
     results in a new Docker image at: <registry_url>/gabrtv/myapp:v23
     which contains the new configuration as ENV entries.
     """
-    image_id = _get_tag(repository_path, 'latest')
+    try:
+        image_id = _get_tag(repository_path, 'latest')
+    except RuntimeError:
+        # no image exists yet, so let's build one!
+        _put_first_image(repository_path)
+        image_id = _get_tag(repository_path, 'latest')
     image = _get_image(image_id)
     # construct the new image
     image['parent'] = image['id']
     image['id'] = _new_id()
     image['config']['Env'] = _construct_env(image['config']['Env'], config)
     # update and tag the new image
-    _put_image(image)
-    cookies = _put_layer(image['id'], _empty_tar_archive())
-    _put_checksum(image, cookies)
-    _put_tag(image['id'], repository_path, tag)
+    _commit(repository_path, image, _empty_tar_archive(), tag)
 
 
 # registry access
 
 
+def _commit(repository_path, image, layer, tag):
+    _put_image(image)
+    cookies = _put_layer(image['id'], layer)
+    _put_checksum(image, cookies)
+    _put_tag(image['id'], repository_path, tag)
+    # point latest to the new tag
+    _put_tag(image['id'], repository_path, 'latest')
+
+
+def _put_first_image(repository_path):
+    image = {
+        'id': _new_id(),
+        'parent': '',
+        'config': {
+            'Env': []
+        }
+    }
+    # tag as v0 in the registry
+    _commit(repository_path, image, _empty_tar_archive(), 'v0')
+
+
 def _get_tag(repository, tag):
     path = "/v1/repositories/{repository}/tags/{tag}".format(**locals())
     url = urlparse.urljoin(settings.REGISTRY_URL, path)


Test patch: