Skip to content

Commit d7e808f

Browse files
author
Gabriel Monroy
committed
Merge pull request #1190 from deis/deis-build
`deis pull` as alternative to `git push` workflow
2 parents c8d8688 + e9ba0c8 commit d7e808f

15 files changed

Lines changed: 132 additions & 42 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ test: test-unit test-functional
4848

4949
test-unit:
5050
@if [ ! -d venv ]; then virtualenv venv; fi
51-
venv/bin/pip install -q -r requirements.txt
51+
venv/bin/pip install -q -r requirements.txt -r dev_requirements.txt
5252
venv/bin/python manage.py test --noinput api
5353

5454
test-functional:

api/models.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ class Meta:
462462
def __str__(self):
463463
return "{0}-v{1}".format(self.app.id, self.version)
464464

465-
def new(self, user, config=None, build=None, summary=None, source_version=None):
465+
def new(self, user, config=None, build=None, summary=None, source_version='latest'):
466466
"""
467467
Create a new application release using the provided Build and Config
468468
on behalf of a user.
@@ -473,24 +473,28 @@ def new(self, user, config=None, build=None, summary=None, source_version=None):
473473
config = self.config
474474
if not build:
475475
build = self.build
476-
if not source_version:
477-
source_version = 'latest'
478-
else:
479-
source_version = 'v{}'.format(source_version)
480-
# prepare release tag
476+
# always create a release off the latest image
477+
source_image = '{}:{}'.format(build.image, source_version)
478+
# construct fully-qualified target image
481479
new_version = self.version + 1
482480
tag = 'v{}'.format(new_version)
483-
image = build.image + ':{tag}'.format(**locals())
481+
release_image = '{}:{}'.format(self.app.id, tag)
482+
target_image = '{}:{}/{}'.format(
483+
settings.REGISTRY_HOST, settings.REGISTRY_PORT, self.app.id)
484484
# create new release and auto-increment version
485485
release = Release.objects.create(
486486
owner=user, app=self.app, config=config,
487-
build=build, version=new_version, image=image, summary=summary)
488-
# publish release to registry as new docker image
489-
repository_path = self.app.id
490-
publish_release(repository_path,
487+
build=build, version=new_version, image=target_image, summary=summary)
488+
# IOW, this image did not come from the builder
489+
if not build.sha:
490+
# we assume that the image is not present on our registry,
491+
# so shell out a task to pull in the repository
492+
tasks.import_repository.delay(build.image, self.app.id).get()
493+
# update the source image to the repository we just imported
494+
source_image = self.app.id
495+
publish_release(source_image,
491496
config.values,
492-
tag,
493-
source_tag=source_version)
497+
release_image,)
494498
return release
495499

496500
def previous(self):
@@ -516,7 +520,9 @@ def save(self, *args, **kwargs):
516520
# compare this build to the previous build
517521
old_build = prev_release.build if prev_release else None
518522
# if the build changed, log it and who pushed it
519-
if self.build != old_build:
523+
if self.version == 1:
524+
self.summary += "{} created initial release".format(self.app.owner)
525+
elif self.build != old_build:
520526
if self.build.sha:
521527
self.summary += "{} deployed {}".format(self.build.owner, self.build.sha[:7])
522528
else:

api/tasks.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
from __future__ import unicode_literals
99

10+
import requests
1011
import threading
1112

1213
from celery import task
14+
from django.conf import settings
1315

1416

1517
@task
@@ -34,6 +36,19 @@ def deploy_release(app, release):
3436
[t.join() for t in threads]
3537

3638

39+
@task
40+
def import_repository(source, target_repository):
41+
"""Imports an image from a remote registry into our own private registry"""
42+
data = {
43+
'src': source,
44+
}
45+
requests.post(
46+
'{}/v1/repositories/{}/tags'.format(settings.REGISTRY_URL,
47+
target_repository),
48+
data=data,
49+
)
50+
51+
3752
@task
3853
def start_containers(containers):
3954
create_threads = []
@@ -71,10 +86,10 @@ def run_command(c, command):
7186
if rc != 0:
7287
raise EnvironmentError('Could not pull image: {pull_image}'.format(**locals()))
7388
# run the command
74-
docker_args = ' '.join(['-a', 'stdout', '-a', 'stderr', '--rm', image])
75-
env_args = ' '.join(["-e '{k}={v}'".format(**locals())
76-
for k, v in release.config.values.items()])
77-
command = "docker run {env_args} {docker_args} {command}".format(**locals())
89+
docker_args = ' '.join(['--entrypoint=/bin/sh',
90+
'-a', 'stdout', '-a', 'stderr', '--rm', image])
91+
escaped_command = command.replace("'", "'\\''")
92+
command = r"docker run {docker_args} -c \'{escaped_command}\'".format(**locals())
7893
return c.run(command)
7994
finally:
8095
c.delete()

api/tests/test_build.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import mock
11+
import requests
1012

1113
from django.test import TransactionTestCase
1214
from django.test.utils import override_settings
1315

1416
from api.models import Build
1517

1618

19+
def mock_import_repository_task(*args, **kwargs):
20+
resp = requests.Response()
21+
resp.status_code = 200
22+
resp._content_consumed = True
23+
return resp
24+
25+
1726
@override_settings(CELERY_ALWAYS_EAGER=True)
1827
class BuildTest(TransactionTestCase):
1928

@@ -30,6 +39,7 @@ def setUp(self):
3039
content_type='application/json')
3140
self.assertEqual(response.status_code, 201)
3241

42+
@mock.patch('requests.post', mock_import_repository_task)
3343
def test_build(self):
3444
"""
3545
Test that a null build is created and that users can post new builds
@@ -71,6 +81,7 @@ def test_build(self):
7181
self.assertEqual(self.client.patch(url).status_code, 405)
7282
self.assertEqual(self.client.delete(url).status_code, 405)
7383

84+
@mock.patch('requests.post', mock_import_repository_task)
7485
def test_build_default_containers(self):
7586
url = '/api/apps'
7687
body = {'cluster': 'autotest'}
@@ -152,6 +163,7 @@ def test_build_default_containers(self):
152163
self.assertEqual(container['type'], 'web')
153164
self.assertEqual(container['num'], 1)
154165

166+
@mock.patch('requests.post', mock_import_repository_task)
155167
def test_build_str(self):
156168
"""Test the text representation of a build."""
157169
url = '/api/apps'

api/tests/test_config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import mock
11+
import requests
1012

1113
from django.test import TransactionTestCase
1214
from django.test.utils import override_settings
1315

1416
from api.models import Config
1517

1618

19+
def mock_import_repository_task(*args, **kwargs):
20+
resp = requests.Response()
21+
resp.status_code = 200
22+
resp._content_consumed = True
23+
return resp
24+
25+
1726
@override_settings(CELERY_ALWAYS_EAGER=True)
1827
class ConfigTest(TransactionTestCase):
1928

@@ -30,6 +39,7 @@ def setUp(self):
3039
content_type='application/json')
3140
self.assertEqual(response.status_code, 201)
3241

42+
@mock.patch('requests.post', mock_import_repository_task)
3343
def test_config(self):
3444
"""
3545
Test that config is auto-created for a new app and that
@@ -94,6 +104,7 @@ def test_config(self):
94104
self.assertEqual(self.client.delete(url).status_code, 405)
95105
return config5
96106

107+
@mock.patch('requests.post', mock_import_repository_task)
97108
def test_config_set_same_key(self):
98109
"""
99110
Test that config sets on the same key function properly
@@ -116,6 +127,7 @@ def test_config_set_same_key(self):
116127
self.assertIn('PORT', json.loads(response.data['values']))
117128
self.assertEqual(json.loads(response.data['values'])['PORT'], '5001')
118129

130+
@mock.patch('requests.post', mock_import_repository_task)
119131
def test_config_str(self):
120132
"""Test the text representation of a node."""
121133
config5 = self.test_config()

api/tests/test_container.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import mock
11+
import requests
1012

1113
from django.contrib.auth.models import User
1214
from django.test import TransactionTestCase
@@ -17,9 +19,15 @@
1719
from api.models import Container, App
1820

1921

22+
def mock_import_repository_task(*args, **kwargs):
23+
resp = requests.Response()
24+
resp.status_code = 200
25+
resp._content_consumed = True
26+
return resp
27+
28+
2029
@override_settings(CELERY_ALWAYS_EAGER=True)
2130
class ContainerTest(TransactionTestCase):
22-
2331
"""Tests creation of containers on nodes"""
2432

2533
fixtures = ['tests.json']
@@ -174,6 +182,7 @@ def test_container_api_heroku(self):
174182
response = self.client.get(url)
175183
self.assertEqual(response.status_code, 200)
176184

185+
@mock.patch('requests.post', mock_import_repository_task)
177186
def test_container_api_docker(self):
178187
url = '/api/apps'
179188
body = {'cluster': 'autotest'}
@@ -233,6 +242,7 @@ def test_container_api_docker(self):
233242
response = self.client.get(url)
234243
self.assertEqual(response.status_code, 200)
235244

245+
@mock.patch('requests.post', mock_import_repository_task)
236246
def test_container_release(self):
237247
url = '/api/apps'
238248
body = {'cluster': 'autotest'}

api/tests/test_hooks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import mock
11+
import requests
1012

1113
from django.test import TransactionTestCase
1214
from django.test.utils import override_settings
1315

1416
from django.conf import settings
1517

1618

19+
def mock_import_repository_task(*args, **kwargs):
20+
resp = requests.Response()
21+
resp.status_code = 200
22+
resp._content_consumed = True
23+
return resp
24+
25+
1726
@override_settings(CELERY_ALWAYS_EAGER=True)
1827
class HookTest(TransactionTestCase):
1928

@@ -95,6 +104,7 @@ def test_push_abuse(self):
95104
HTTP_X_DEIS_BUILDER_AUTH=settings.BUILDER_KEY)
96105
self.assertEqual(response.status_code, 403)
97106

107+
@mock.patch('requests.post', mock_import_repository_task)
98108
def test_build_hook(self):
99109
"""Test creating a Build via an API Hook"""
100110
url = '/api/apps'

api/tests/test_release.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import mock
11+
import requests
1012

1113
from django.test import TransactionTestCase
1214
from django.test.utils import override_settings
1315

1416
from api.models import Release
1517

1618

19+
def mock_import_repository_task(*args, **kwargs):
20+
resp = requests.Response()
21+
resp.status_code = 200
22+
resp._content_consumed = True
23+
return resp
24+
25+
1726
@override_settings(CELERY_ALWAYS_EAGER=True)
1827
class ReleaseTest(TransactionTestCase):
1928

@@ -30,6 +39,7 @@ def setUp(self):
3039
content_type='application/json')
3140
self.assertEqual(response.status_code, 201)
3241

42+
@mock.patch('requests.post', mock_import_repository_task)
3343
def test_release(self):
3444
"""
3545
Test that a release is created when a cluster is created, and
@@ -101,6 +111,7 @@ def test_release(self):
101111
self.assertEqual(self.client.delete(url).status_code, 405)
102112
return release3
103113

114+
@mock.patch('requests.post', mock_import_repository_task)
104115
def test_release_rollback(self):
105116
url = '/api/apps'
106117
body = {'cluster': 'autotest'}
@@ -187,12 +198,14 @@ def test_release_rollback(self):
187198
self.assertIn('NEW_URL1', values)
188199
self.assertEqual('http://localhost:8080/', values['NEW_URL1'])
189200

201+
@mock.patch('requests.post', mock_import_repository_task)
190202
def test_release_str(self):
191203
"""Test the text representation of a release."""
192204
release3 = self.test_release()
193205
release = Release.objects.get(uuid=release3['uuid'])
194206
self.assertEqual(str(release), "{}-v3".format(release3['app']))
195207

208+
@mock.patch('requests.post', mock_import_repository_task)
196209
def test_release_summary(self):
197210
"""Test the text summary of a release."""
198211
release3 = self.test_release()

api/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,10 +428,10 @@ def rollback(self, request, *args, **kwargs):
428428
build=prev.build,
429429
config=prev.config,
430430
summary=summary,
431-
source_version=version)
431+
source_version='v{}'.format(version))
432432
app.deploy(new_release)
433-
msg = "Rolled back to v{}".format(version)
434-
return Response(msg, status=status.HTTP_201_CREATED)
433+
response = {'version': new_release.version}
434+
return Response(response, status=status.HTTP_201_CREATED)
435435

436436

437437
class AppContainerViewSet(OwnerViewSet):

deis/settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,9 @@
278278

279279
# registry settings
280280
REGISTRY_MODULE = 'registry.mock'
281-
REGISTRY_URL = os.environ.get('DEIS_REGISTRY_URL', None)
281+
REGISTRY_URL = 'http://localhost:5000'
282+
REGISTRY_HOST = 'localhost'
283+
REGISTRY_PORT = 5000
282284

283285
# check if we can register users with `deis register`
284286
REGISTRATION_ENABLED = True

0 commit comments

Comments
 (0)