Skip to content

Commit 9beb70e

Browse files
author
Gabriel Monroy
committed
Merge pull request #1190 from deis/deis-build
`deis pull` as alternative to `git push` workflow
2 parents 27a9ac8 + 74d334c commit 9beb70e

20 files changed

Lines changed: 234 additions & 92 deletions

File tree

builder/templates/builder

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,17 @@ ENTRYPOINT ["/runner/init"]
3434

3535
if __name__ == '__main__':
3636
user, repo, branch, app = parse_args()
37-
# define image names
38-
_id = uuid.uuid4().hex[:8]
39-
tmp_image = "{app}:temp_{_id}".format(**locals())
40-
target_image = "{{ .deis_registry_host }}:{{ .deis_registry_port }}/{app}".format(**locals())
4137
# create required directories
4238
repo_dir = os.path.join(os.getcwd(), repo)
4339
build_dir = os.path.join(repo_dir, 'build')
4440
cache_dir = os.path.join(repo_dir, 'cache')
41+
# get sha of branch
42+
with open(os.path.join(repo_dir, branch)) as f:
43+
sha = f.read().strip('\n')
44+
short_sha = sha[:8]
45+
# define image names
46+
tmp_image = "{app}:git-{short_sha}".format(**locals())
47+
target_image = "{{ .deis_registry_host }}:{{ .deis_registry_port }}/{app}".format(**locals())
4548
for d in (cache_dir, build_dir):
4649
if not os.path.exists(d):
4750
os.mkdir(d)
@@ -111,6 +114,7 @@ if __name__ == '__main__':
111114
raise Exception('Could not push Docker image')
112115
# construct json body for posting to the build hook
113116
body = {}
117+
body['sha'] = sha
114118
body['receive_user'] = user
115119
body['receive_repo'] = app
116120
body['image'] = target_image
@@ -162,8 +166,8 @@ if __name__ == '__main__':
162166
l = locals()
163167
shutil.rmtree(temp_dir)
164168
if 'container' in l:
165-
subprocess.Popen('docker rm {container}'.format(**l), shell=True,
169+
subprocess.Popen('docker rm -f {container}'.format(**l), shell=True,
166170
stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
167-
if 'tmp_image' in l:
168-
subprocess.Popen('docker rmi {tmp_image}'.format(**l), shell=True,
171+
if 'tmp_image' in l and 'target_image' in l:
172+
subprocess.Popen('docker rmi -f {tmp_image} {target_image}'.format(**l), shell=True,
169173
stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))

client/deis.py

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,30 @@ def __init__(self):
9292
self.cookies.clear_expired_cookies()
9393
self.cookies.save()
9494

95+
@property
96+
def app(self):
97+
"""Retrieve the application's name."""
98+
try:
99+
return self._get_name_from_git_remote(self.git_root())
100+
except EnvironmentError:
101+
return os.path.basename(os.getcwd())
102+
103+
def is_git_app(self):
104+
"""Determines if this app is a git repository. This is important in special cases
105+
where we need to know whether or not we should use Deis' automatic app name
106+
generator, for example.
107+
"""
108+
try:
109+
self.git_root()
110+
return True
111+
except EnvironmentError:
112+
return False
113+
95114
def git_root(self):
96115
"""
97-
Return the absolute path from the git repository root
116+
Returns the absolute path from the git repository root.
98117
99-
If no git repository exists, raise an EnvironmentError
118+
If no git repository exists, raises an EnvironmentError.
100119
"""
101120
try:
102121
git_root = subprocess.check_output(
@@ -106,15 +125,13 @@ def git_root(self):
106125
raise EnvironmentError('Current directory is not a git repository')
107126
return git_root
108127

109-
def get_app(self):
128+
def _get_name_from_git_remote(self, git_root):
110129
"""
111-
Return the application name for the current directory
130+
Retrieves the application name from a git repository root.
112131
113132
The application is determined by parsing `git remote -v` output.
114-
If no application is found, raise an EnvironmentError.
133+
If no application is found, raises an EnvironmentError.
115134
"""
116-
git_root = self.git_root()
117-
# try to match a deis remote
118135
remotes = subprocess.check_output(['git', 'remote', '-v'],
119136
cwd=git_root)
120137
m = re.search(r'^deis\W+(?P<url>\S+)\W+\(', remotes, re.MULTILINE)
@@ -127,8 +144,6 @@ def get_app(self):
127144
raise EnvironmentError("Could not parse: {url}".format(**locals()))
128145
return m.groupdict()['app']
129146

130-
app = property(get_app)
131-
132147
def request(self, *args, **kwargs):
133148
"""
134149
Issue an HTTP request with proper cookie handling including
@@ -391,19 +406,13 @@ def apps_create(self, args):
391406
--cluster=CLUSTER target cluster to host application [default: dev]
392407
--no-remote do not create a 'deis' git remote
393408
"""
394-
try:
395-
self._session.git_root() # check for a git repository
396-
except EnvironmentError:
397-
print('No git repository found, use `git init` to create one')
398-
sys.exit(1)
399-
try:
400-
self._session.get_app()
401-
print('Deis remote already exists')
402-
sys.exit(1)
403-
except EnvironmentError:
404-
pass
405409
body = {}
406-
app_name = args.get('<id>')
410+
app_name = None
411+
if not self._session.is_git_app():
412+
app_name = self._session.app
413+
# prevent app name from being reset to None
414+
if args.get('<id>'):
415+
app_name = args.get('<id>')
407416
if app_name:
408417
body.update({'id': app_name})
409418
cluster = args.get('--cluster')
@@ -423,8 +432,11 @@ def apps_create(self, args):
423432
data = response.json()
424433
app_id = data['id']
425434
print("done, created {}".format(app_id))
426-
# add a git remote
427-
# TODO: retrieve the hostname from service discovery
435+
# set a git remote if necessary
436+
try:
437+
self._session.git_root()
438+
except EnvironmentError:
439+
return
428440
hostname = urlparse.urlparse(self._settings['controller']).netloc.split(':')[0]
429441
git_remote = "ssh://git@{hostname}:2222/{app_id}.git".format(**locals())
430442
if args.get('--no-remote'):
@@ -476,9 +488,9 @@ def apps_destroy(self, args):
476488
if response.status_code in (requests.codes.no_content, # @UndefinedVariable
477489
requests.codes.not_found): # @UndefinedVariable
478490
print('done in {}s'.format(int(time.time() - before)))
479-
# If the requested app is in the current dir, delete the git remote
480491
try:
481-
if app == self._session.app:
492+
# If the requested app is a heroku app, delete the git remote
493+
if self._session.is_git_app():
482494
subprocess.check_call(
483495
['git', 'remote', 'rm', 'deis'],
484496
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -729,9 +741,19 @@ def builds(self, args):
729741

730742
def builds_create(self, args):
731743
"""
732-
Create a new build of an application
744+
Creates a new build of an application. Imports an <image> and deploys it to Deis
745+
as a new release.
733746
734747
Usage: deis builds:create <image> [--app=<app>]
748+
749+
Arguments:
750+
<image>
751+
A fully-qualified docker image, either from DockerHub (e.g. deis/example-go)
752+
or from an in-house registry (e.g. myregistry.example.com:5000/example-go).
753+
754+
Options:
755+
--app=<app>
756+
The uniquely identifiable name for the application.
735757
"""
736758
app = args.get('--app')
737759
if not app:
@@ -1178,12 +1200,13 @@ def ps_scale(self, args):
11781200
"""
11791201
app = args.get('--app')
11801202
if not app:
1181-
app = self._session.get_app()
1203+
app = self._session.app
11821204
body = {}
11831205
for type_num in args.get('<type=num>'):
11841206
typ, count = type_num.split('=')
11851207
body.update({typ: int(count)})
1186-
print('Scaling processes... but first, coffee!')
1208+
sys.stdout.write('Scaling processes... but first, coffee!\n')
1209+
sys.stdout.flush()
11871210
try:
11881211
progress = TextProgress()
11891212
progress.start()
@@ -1195,7 +1218,7 @@ def ps_scale(self, args):
11951218
progress.cancel()
11961219
progress.join()
11971220
if response.status_code == requests.codes.no_content: # @UndefinedVariable
1198-
print('done in {}s\n'.format(int(time.time() - before)))
1221+
print('done in {}s'.format(int(time.time() - before)))
11991222
self.ps_list({}, app)
12001223
else:
12011224
raise ResponseError(response)
@@ -1449,7 +1472,7 @@ def releases_list(self, args):
14491472
data = response.json()
14501473
for item in data['results']:
14511474
item['created'] = readable_datetime(item['created'])
1452-
print("v{version:<6} {created:<33} {summary}".format(**item))
1475+
print("v{version:<6} {created:<24} {summary}".format(**item))
14531476
else:
14541477
raise ResponseError(response)
14551478

@@ -1470,9 +1493,18 @@ def releases_rollback(self, args):
14701493
else:
14711494
body = {}
14721495
url = "/api/apps/{app}/releases/rollback".format(**locals())
1473-
response = self._dispatch('post', url, json.dumps(body))
1474-
if response.status_code == requests.codes.created:
1475-
print(response.json())
1496+
sys.stdout.write('Rolling back to v{version}... '.format(**locals()))
1497+
sys.stdout.flush()
1498+
try:
1499+
progress = TextProgress()
1500+
progress.start()
1501+
response = self._dispatch('post', url, json.dumps(body))
1502+
finally:
1503+
progress.cancel()
1504+
progress.join()
1505+
if response.status_code == requests.codes.created: # @UndefinedVariable
1506+
new_version = response.json()['version']
1507+
print("done, v{}".format(new_version))
14761508
else:
14771509
raise ResponseError(response)
14781510

@@ -1499,6 +1531,7 @@ def shortcuts(self, args):
14991531
('register', 'auth:register'),
15001532
('login', 'auth:login'),
15011533
('logout', 'auth:logout'),
1534+
('pull', 'builds:create'),
15021535
('scale', 'ps:scale'),
15031536
('rollback', 'releases:rollback'),
15041537
('sharing', 'perms:list'),

controller/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:

controller/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:

controller/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()

0 commit comments

Comments
 (0)