Skip to content

Commit e0ba0a9

Browse files
author
Gabriel Monroy
committed
feat(builder): expose runtime configuration during slugbuilder execution
This uses a new Config API Hook to grab the latest app configuration and expose it to slugbuilder using `-e` flags. While this works, it is a very good mechanism for handling user-defined inputs on the builder. In other words, command-injection is possible through malformed inputs. We must consider this as we move to refactor the builder in Go.
1 parent 1174bf0 commit e0ba0a9

4 files changed

Lines changed: 63 additions & 2 deletions

File tree

builder/templates/builder

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,21 @@ if __name__ == '__main__':
5656
if rc != 0:
5757
raise Exception('Could not extract git archive')
5858
dockerfile = os.path.join(temp_dir, 'Dockerfile')
59+
# pull config to be used during build
60+
body = {}
61+
body['receive_user'], body['receive_repo'] = user, app
62+
url = "{{ .deis_controller_protocol }}://{{ .deis_controller_host }}:{{ .deis_controller_port }}/api/hooks/config"
63+
headers = {'Content-Type': 'application/json', 'X-Deis-Builder-Auth': '{{ .deis_controller_builderKey }}'}
64+
r = requests.post(url, headers=headers, data=json.dumps(body))
65+
if r.status_code != 200:
66+
raise Exception('Config hook error: {} {}'.format(r.status_code, r.text))
67+
config_env = " ".join([ "-e {}='{}'".format(*kv) for kv in json.loads(r.json().get('values', '{}')).items()])
5968
# some applications do not have a Procfile, so only check for a Dockerfile
6069
if not os.path.exists(dockerfile):
6170
if os.path.exists('/buildpacks'):
62-
build_cmd = "docker run -i -a stdin -v {cache_dir}:/tmp/cache:rw -v /buildpacks:/tmp/buildpacks deis/slugbuilder".format(**locals())
71+
build_cmd = "docker run -i -a stdin {config_env} -v {cache_dir}:/tmp/cache:rw -v /buildpacks:/tmp/buildpacks deis/slugbuilder".format(**locals())
6372
else:
64-
build_cmd = "docker run -i -a stdin -v {cache_dir}:/tmp/cache:rw deis/slugbuilder".format(**locals())
73+
build_cmd = "docker run -i -a stdin {config_env} -v {cache_dir}:/tmp/cache:rw deis/slugbuilder".format(**locals())
6574
# run slugbuilder in the background
6675
p = subprocess.Popen("git archive master | " + build_cmd, shell=True, cwd=repo_dir, stdout=subprocess.PIPE)
6776
container = p.stdout.read().strip('\n')

controller/api/tests/test_hooks.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,31 @@ def test_build_hook(self):
118118
self.assertIn('release', response.data)
119119
self.assertIn('version', response.data['release'])
120120
self.assertIn('domains', response.data)
121+
122+
def test_config_hook(self):
123+
"""Test creating a Config via an API Hook"""
124+
url = '/api/apps'
125+
body = {'cluster': 'autotest'}
126+
response = self.client.post(url, json.dumps(body), content_type='application/json')
127+
self.assertEqual(response.status_code, 201)
128+
app_id = response.data['id']
129+
url = '/api/apps/{app_id}/config'.format(**locals())
130+
response = self.client.get(url)
131+
self.assertEqual(response.status_code, 200)
132+
self.assertIn('values', response.data)
133+
values = response.data['values']
134+
# prepare the config hook
135+
config = {'username': 'autotest', 'app': app_id}
136+
url = '/api/hooks/config'.format(**locals())
137+
body = {'receive_user': 'autotest',
138+
'receive_repo': app_id}
139+
# post without a session
140+
self.assertIsNone(self.client.logout())
141+
response = self.client.post(url, json.dumps(body), content_type='application/json')
142+
self.assertEqual(response.status_code, 403)
143+
# post with the builder auth key
144+
response = self.client.post(url, json.dumps(body), content_type='application/json',
145+
HTTP_X_DEIS_BUILDER_AUTH=settings.BUILDER_KEY)
146+
self.assertEqual(response.status_code, 200)
147+
self.assertIn('values', response.data)
148+
self.assertEqual(values, response.data['values'])

controller/api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@
165165
166166
Create a new :class:`~api.models.Build`.
167167
168+
.. http:post:: /api/hooks/config/
169+
170+
Retrieve latest application :class:`~api.models.Config`.
171+
168172
169173
Auth
170174
====
@@ -279,6 +283,8 @@
279283
views.PushHookViewSet.as_view({'post': 'create'})),
280284
url(r'^hooks/build/?',
281285
views.BuildHookViewSet.as_view({'post': 'create'})),
286+
url(r'^hooks/config/?',
287+
views.ConfigHookViewSet.as_view({'post': 'create'})),
282288
# authn / authz
283289
url(r'^auth/register/?',
284290
views.UserRegistrationView.as_view({'post': 'create'})),

controller/api/views.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,3 +512,21 @@ def post_save(self, build, created=False):
512512
release = build.app.release_set.latest()
513513
new_release = release.new(build.owner, build=build)
514514
build.app.deploy(new_release)
515+
516+
517+
class ConfigHookViewSet(BaseHookViewSet):
518+
"""API hook to grab latest :class:`~api.models.Config`"""
519+
520+
model = models.Config
521+
serializer_class = serializers.ConfigSerializer
522+
523+
def create(self, request, *args, **kwargs):
524+
app = get_object_or_404(models.App, id=request.DATA['receive_repo'])
525+
user = get_object_or_404(
526+
User, username=request.DATA['receive_user'])
527+
# check the user is authorized for this app
528+
if user == app.owner or user in get_users_with_perms(app):
529+
config = app.release_set.latest().config
530+
serializer = self.get_serializer(config)
531+
return Response(serializer.data, status=status.HTTP_200_OK)
532+
raise PermissionDenied()

0 commit comments

Comments
 (0)