Repository: deis/deis

Issue:
Expose config variables during build
This is sometimes needed when the build step (eg. Rails assets:precompile) needs to access the environment.
In my case this is needed when using rails asset_sync gem to deploy assets to S3 (https://github.com/rumblelabs/asset_sync)

Heroku adds an option for this (https://devcenter.heroku.com/articles/labs-user-env-compile).
An alternative is not provided (AFAIK) by deis (0.4).

This results in the fact that my app can not be deployed to Deis powered PaaS.



Discussion:
The asset_sync has the possibility to not read config from the environment but hardcode them in the configuration. (possibly split between environments).
Obviously this is a last resort but goes against 12 factor app practices.

Apparantly https://devcenter.heroku.com/articles/using-amazon-cloudfront-cdn-with-rails is now preferred (even on heroku), which will solve one of my issues.

Still I think the issue should not be closed since even the ruby buildpack uses ENV flags (like BUNDL_WITHOUT -- https://github.com/heroku/heroku-buildpack-ruby/blob/master/lib/language_pack/ruby.rb#L480) which are now ignored during compilation

Other usecases for this: providing credentials for a private package registry and configuring build options like `DISABLE_COLLECTSTATIC` in [heroku-buildpack-python](https://github.com/heroku/heroku-buildpack-python/blob/3dde375d0be519e7270fe80f97a40d9aa68a1fcc/bin/steps/collectstatic#L12).

As i understand it this is now actually default behaviour on heroku
although with a slight twist and a change to buildpacks
https://devcenter.heroku.com/changelog-items/416 for more info
Op 27 feb. 2014 22:00 schreef "Matthew Dapena-Tretter" <
notifications@github.com>:

> Other usecases for this: providing credentials for a private package
> registry, configuring build options like DISABLE_COLLECTSTATIC in
> heroku-buildpack-pythonhttps://github.com/heroku/heroku-buildpack-python/blob/3dde375d0be519e7270fe80f97a40d9aa68a1fcc/bin/steps/collectstatic#L12
> .
> 
> ## 
> 
> Reply to this email directly or view it on GitHubhttps://github.com/opdemand/deis/issues/462#issuecomment-36290731
> .

I'm working on this: [API changes](https://github.com/tombh/deis/tree/env_vars_during_build) and [Builder changes](https://github.com/tombh/builder/tree/env_vars_during_build)

It seems to be working as I can get it to use a custom buildpack by using `deis config:set BUILDPACK_URL=http://mycoolbuildpack.com`

I'll PR once I've written some tests.


Resolving patch:
diff --git a/controller/api/models.py b/controller/api/models.py
--- a/controller/api/models.py
+++ b/controller/api/models.py
@@ -129,11 +129,6 @@ def delete(self, *args, **kwargs):
 
     def deploy(self, release):
         tasks.deploy_release.delay(self, release).get()
-        if self.structure == {}:
-            # scale the web process by 1 initially
-            self.structure = {'web': 1}
-            self.save()
-            self.scale()
 
     def destroy(self, *args, **kwargs):
         return self.delete(*args, **kwargs)
diff --git a/controller/api/urls.py b/controller/api/urls.py
--- a/controller/api/urls.py
+++ b/controller/api/urls.py
@@ -165,6 +165,10 @@
 
   Create a new :class:`~api.models.Build`.
 
+.. http:post:: /api/hooks/config/
+
+  Retrieve latest application :class:`~api.models.Config`.
+
 
 Auth
 ====
@@ -279,6 +283,8 @@
         views.PushHookViewSet.as_view({'post': 'create'})),
     url(r'^hooks/build/?',
         views.BuildHookViewSet.as_view({'post': 'create'})),
+    url(r'^hooks/config/?',
+        views.ConfigHookViewSet.as_view({'post': 'create'})),
     # authn / authz
     url(r'^auth/register/?',
         views.UserRegistrationView.as_view({'post': 'create'})),
diff --git a/controller/api/views.py b/controller/api/views.py
--- a/controller/api/views.py
+++ b/controller/api/views.py
@@ -345,6 +345,11 @@ def post_save(self, build, created=False):
             release = build.app.release_set.latest()
             self.release = release.new(self.request.user, build=build)
             build.app.deploy(self.release)
+            # scale the web process by 1 initially
+            if build.app.structure == {}:
+                build.app.structure = {'web': 1}
+                build.app.save()
+                build.app.scale()
 
     def get_success_headers(self, data):
         headers = super(AppBuildViewSet, self).get_success_headers(data)
@@ -512,3 +517,21 @@ def post_save(self, build, created=False):
             release = build.app.release_set.latest()
             new_release = release.new(build.owner, build=build)
             build.app.deploy(new_release)
+
+
+class ConfigHookViewSet(BaseHookViewSet):
+    """API hook to grab latest :class:`~api.models.Config`"""
+
+    model = models.Config
+    serializer_class = serializers.ConfigSerializer
+
+    def create(self, request, *args, **kwargs):
+        app = get_object_or_404(models.App, id=request.DATA['receive_repo'])
+        user = get_object_or_404(
+            User, username=request.DATA['receive_user'])
+        # check the user is authorized for this app
+        if user == app.owner or user in get_users_with_perms(app):
+            config = app.release_set.latest().config
+            serializer = self.get_serializer(config)
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        raise PermissionDenied()


Test patch:
diff --git a/controller/api/tests/test_hooks.py b/controller/api/tests/test_hooks.py
--- a/controller/api/tests/test_hooks.py
+++ b/controller/api/tests/test_hooks.py
@@ -118,3 +118,31 @@ def test_build_hook(self):
         self.assertIn('release', response.data)
         self.assertIn('version', response.data['release'])
         self.assertIn('domains', response.data)
+
+    def test_config_hook(self):
+        """Test reading Config via an API Hook"""
+        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())
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('values', response.data)
+        values = response.data['values']
+        # prepare the config hook
+        config = {'username': 'autotest', 'app': app_id}
+        url = '/api/hooks/config'.format(**locals())
+        body = {'receive_user': 'autotest',
+                'receive_repo': app_id}
+        # post without a session
+        self.assertIsNone(self.client.logout())
+        response = self.client.post(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 403)
+        # post with the builder auth key
+        response = self.client.post(url, json.dumps(body), content_type='application/json',
+                                    HTTP_X_DEIS_BUILDER_AUTH=settings.BUILDER_KEY)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('values', response.data)
+        self.assertEqual(values, response.data['values'])