Flesh out the create API and wire everything up together. Next up, testing.
This commit is contained in:
parent
2afb8c85b1
commit
9b9a29c310
10 changed files with 156 additions and 15 deletions
|
@ -3,6 +3,7 @@ import logging
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
|
|
||||||
from flask import Flask, request, send_file, jsonify, redirect, url_for, abort
|
from flask import Flask, request, send_file, jsonify, redirect, url_for, abort
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
@ -39,7 +40,7 @@ def prepare_zip(request_file):
|
||||||
|
|
||||||
# Save the zip file to temp somewhere
|
# Save the zip file to temp somewhere
|
||||||
with TemporaryFile() as zip_file:
|
with TemporaryFile() as zip_file:
|
||||||
request_file.save(zip_file)
|
zip_file.write(request_file.content)
|
||||||
to_extract = ZipFile(zip_file)
|
to_extract = ZipFile(zip_file)
|
||||||
to_extract.extractall(build_dir)
|
to_extract.extractall(build_dir)
|
||||||
|
|
||||||
|
@ -49,7 +50,8 @@ def prepare_zip(request_file):
|
||||||
def prepare_dockerfile(request_file):
|
def prepare_dockerfile(request_file):
|
||||||
build_dir = mkdtemp(prefix='docker-build-')
|
build_dir = mkdtemp(prefix='docker-build-')
|
||||||
dockerfile_path = os.path.join(build_dir, "Dockerfile")
|
dockerfile_path = os.path.join(build_dir, "Dockerfile")
|
||||||
request_file.save(dockerfile_path)
|
with open(dockerfile_path, 'w') as dockerfile:
|
||||||
|
dockerfile.write(request_file.content)
|
||||||
|
|
||||||
return build_dir
|
return build_dir
|
||||||
|
|
||||||
|
@ -141,10 +143,12 @@ pool = ThreadPool(1)
|
||||||
|
|
||||||
@app.route('/build/', methods=['POST'])
|
@app.route('/build/', methods=['POST'])
|
||||||
def start_build():
|
def start_build():
|
||||||
docker_input = request.files['dockerfile']
|
resource_url = request.values['resource_url']
|
||||||
c_type = docker_input.content_type
|
|
||||||
tag_name = request.values['tag']
|
tag_name = request.values['tag']
|
||||||
|
|
||||||
|
download_resource = requests.get(resource_url)
|
||||||
|
download_resource.get()
|
||||||
|
|
||||||
logger.info('Request to build file of type: %s with tag: %s' %
|
logger.info('Request to build file of type: %s with tag: %s' %
|
||||||
(c_type, tag_name))
|
(c_type, tag_name))
|
||||||
|
|
||||||
|
@ -175,7 +179,9 @@ def start_build():
|
||||||
pool.apply_async(build_image, [build_dir, tag_name, num_steps,
|
pool.apply_async(build_image, [build_dir, tag_name, num_steps,
|
||||||
result_object])
|
result_object])
|
||||||
|
|
||||||
return redirect(url_for('get_status', job_id=job_id))
|
resp = make_response('Created', 201)
|
||||||
|
resp.headers['Location'] = url_for('get_status', job_id=job_id)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@app.route('/build/<job_id>')
|
@app.route('/build/<job_id>')
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
flask
|
flask
|
||||||
|
requests
|
||||||
-e git+git://github.com/DevTable/docker-py.git#egg=docker-py
|
-e git+git://github.com/DevTable/docker-py.git#egg=docker-py
|
||||||
|
|
|
@ -42,10 +42,13 @@ class RDSMySQL(object):
|
||||||
DB_DRIVER = MySQLDatabase
|
DB_DRIVER = MySQLDatabase
|
||||||
|
|
||||||
|
|
||||||
class S3Storage(object):
|
class AWSCredentials(object):
|
||||||
AWS_ACCESS_KEY = 'AKIAJWZWUIS24TWSMWRA'
|
AWS_ACCESS_KEY = 'AKIAJWZWUIS24TWSMWRA'
|
||||||
AWS_SECRET_KEY = 'EllGwP+noVvzmsUGQJO1qOMk3vm10Vg+UE6xmmpw'
|
AWS_SECRET_KEY = 'EllGwP+noVvzmsUGQJO1qOMk3vm10Vg+UE6xmmpw'
|
||||||
REGISTRY_S3_BUCKET = 'quay-registry'
|
REGISTRY_S3_BUCKET = 'quay-registry'
|
||||||
|
|
||||||
|
|
||||||
|
class S3Storage(AWSCredentials):
|
||||||
STORAGE_KIND = 's3'
|
STORAGE_KIND = 's3'
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,11 +92,12 @@ class DigitalOceanConfig():
|
||||||
DO_CLIENT_ID = 'LJ44y2wwYj1MD0BRxS6qHA'
|
DO_CLIENT_ID = 'LJ44y2wwYj1MD0BRxS6qHA'
|
||||||
DO_CLIENT_SECRET = 'b9357a6f6ff45a33bb03f6dbbad135f9'
|
DO_CLIENT_SECRET = 'b9357a6f6ff45a33bb03f6dbbad135f9'
|
||||||
DO_SSH_KEY_ID = '46986'
|
DO_SSH_KEY_ID = '46986'
|
||||||
|
DO_SSH_PRIVATE_KEY_FILENAME = 'certs/digital_ocean'
|
||||||
|
|
||||||
|
|
||||||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
||||||
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig,
|
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig,
|
||||||
DigitalOceanConfig):
|
DigitalOceanConfig, AWSCredentials):
|
||||||
REGISTRY_SERVER = 'localhost:5000'
|
REGISTRY_SERVER = 'localhost:5000'
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
'level': logging.DEBUG,
|
'level': logging.DEBUG,
|
||||||
|
|
|
@ -151,10 +151,11 @@ class RepositoryTag(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class RepositoryBuild(BaseModel):
|
class RepositoryBuild(BaseModel):
|
||||||
|
repository = ForeignKeyField(Repository)
|
||||||
|
resource_key = CharField()
|
||||||
digitalocean_build_node_id = IntegerField(null=True)
|
digitalocean_build_node_id = IntegerField(null=True)
|
||||||
phase = CharField(default='waiting')
|
phase = CharField(default='waiting')
|
||||||
status_url = CharField(null=True)
|
status_url = CharField(null=True)
|
||||||
repository = ForeignKeyField(Repository)
|
|
||||||
|
|
||||||
|
|
||||||
class QueueItem(BaseModel):
|
class QueueItem(BaseModel):
|
||||||
|
|
|
@ -287,8 +287,8 @@ def set_repository_visibility(repo, visibility):
|
||||||
repo.save()
|
repo.save()
|
||||||
|
|
||||||
|
|
||||||
def create_repository(namespace, name, owner):
|
def create_repository(namespace, name, owner, visibility='private'):
|
||||||
private = Visibility.get(name='private')
|
private = Visibility.get(name=visibility)
|
||||||
repo = Repository.create(namespace=namespace, name=name,
|
repo = Repository.create(namespace=namespace, name=name,
|
||||||
visibility=private)
|
visibility=private)
|
||||||
admin = Role.get(name='admin')
|
admin = Role.get(name='admin')
|
||||||
|
@ -560,3 +560,7 @@ def get_repository_build(request_dbid):
|
||||||
except RepositoryBuild.DoesNotExist:
|
except RepositoryBuild.DoesNotExist:
|
||||||
msg = 'Unable to locate a build by id: %s' % request_dbid
|
msg = 'Unable to locate a build by id: %s' % request_dbid
|
||||||
raise InvalidRepositoryBuildException(msg)
|
raise InvalidRepositoryBuildException(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def create_repository_build(repo, resource_key):
|
||||||
|
return RepositoryBuild.create(repository=repo, resource_key=resource_key)
|
||||||
|
|
34
data/userfiles.py
Normal file
34
data/userfiles.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import boto
|
||||||
|
import os
|
||||||
|
|
||||||
|
from boto.s3.key import Key
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
||||||
|
class S3FileWriteException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserRequestFiles(object):
|
||||||
|
def __init__(self, s3_access_key, s3_secret_key, bucket_name):
|
||||||
|
self._s3_conn = boto.s3.connection.S3Connection(s3_access_key,
|
||||||
|
s3_secret_key,
|
||||||
|
is_secure=False)
|
||||||
|
self._bucket = self._s3_conn.get_bucket(bucket_name)
|
||||||
|
self._prefix = 'userfiles'
|
||||||
|
|
||||||
|
def store_file(self, flask_file):
|
||||||
|
file_id = str(uuid4())
|
||||||
|
full_key = os.path.join(self._prefix, file_id)
|
||||||
|
k = Key(full_key)
|
||||||
|
bytes_written = k.set_contents_from_file(flask_file)
|
||||||
|
|
||||||
|
if bytes_written == 0:
|
||||||
|
raise S3FileWriteException('Unable to write file to S3')
|
||||||
|
|
||||||
|
return file_id
|
||||||
|
|
||||||
|
def get_file_url(self, file_id, expires_in=300):
|
||||||
|
full_key = os.path.join(self._prefix, file_id)
|
||||||
|
k = Key(full_key)
|
||||||
|
return k.generate_url(expires_in)
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
import stripe
|
import stripe
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from flask import request, make_response, jsonify, abort
|
from flask import request, make_response, jsonify, abort, url_for
|
||||||
from flask.ext.login import login_required, current_user, logout_user
|
from flask.ext.login import login_required, current_user, logout_user
|
||||||
from flask.ext.principal import identity_changed, AnonymousIdentity
|
from flask.ext.principal import identity_changed, AnonymousIdentity
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
@ -11,6 +11,8 @@ from collections import defaultdict
|
||||||
import storage
|
import storage
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
|
from data.userfiles import UserRequestFiles
|
||||||
|
from data.queue import dockerfile_build_queue
|
||||||
from app import app
|
from app import app
|
||||||
from util.email import send_confirmation_email, send_recovery_email
|
from util.email import send_confirmation_email, send_recovery_email
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
@ -170,10 +172,34 @@ def get_matching_users(prefix):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
user_files = UserRequestFiles(app.config['AWS_ACCESS_KEY'],
|
||||||
|
app.config['AWS_SECRET_KEY'],
|
||||||
|
app.config['REGISTRY_S3_BUCKET'])
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/', methods=['POST'])
|
@app.route('/api/repository/', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def create_repo_api():
|
def create_repo_api():
|
||||||
pass
|
namespace_name = request.values['namespace']
|
||||||
|
repository_name = request.values['repository']
|
||||||
|
visibility = request.values['visibility']
|
||||||
|
|
||||||
|
owner = current_user.db_user()
|
||||||
|
repo = model.create_repository(namespace_name, repository_name, owner,
|
||||||
|
visibility)
|
||||||
|
|
||||||
|
if request.values['initialize']:
|
||||||
|
logger.debug('User requested repository initialization.')
|
||||||
|
dockerfile_source = request.files['initializedata']
|
||||||
|
dockerfile_id = user_files.store_file(dockerfile_source)
|
||||||
|
|
||||||
|
build_request = model.create_repository_build(repo, dockerfile_id)
|
||||||
|
dockerfile_build_queue.put(json.dumps({'request_id': build_request.id}))
|
||||||
|
|
||||||
|
resp = make_response('Created', 201)
|
||||||
|
resp.headers['Location'] = url_for('get_repo_api', namespace=namespace_name,
|
||||||
|
repository=repository_name)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/find/repository', methods=['GET'])
|
@app.route('/api/find/repository', methods=['GET'])
|
||||||
|
|
|
@ -14,4 +14,5 @@ mixpanel-py
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
marisa-trie
|
marisa-trie
|
||||||
apscheduler
|
apscheduler
|
||||||
python-daemon
|
python-daemon
|
||||||
|
paramiko
|
Binary file not shown.
|
@ -4,11 +4,13 @@ import daemon
|
||||||
import time
|
import time
|
||||||
import argparse
|
import argparse
|
||||||
import digitalocean
|
import digitalocean
|
||||||
|
import requests
|
||||||
|
|
||||||
from apscheduler.scheduler import Scheduler
|
from apscheduler.scheduler import Scheduler
|
||||||
from multiprocessing.pool import ThreadPool
|
from multiprocessing.pool import ThreadPool
|
||||||
|
|
||||||
from data.queue import dockerfile_build_queue
|
from data.queue import dockerfile_build_queue
|
||||||
|
from data.userfiles import UserRequestFiles
|
||||||
from data import model
|
from data import model
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
|
@ -22,6 +24,21 @@ formatter = logging.Formatter(FORMAT)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def try_connection(url, retries=5, period=5):
|
||||||
|
try:
|
||||||
|
return requests.get(url)
|
||||||
|
except ConnectionError as ex:
|
||||||
|
if retries:
|
||||||
|
logger.debug('Retrying connection to url: %s after %ss' % (url, period))
|
||||||
|
time.sleep(period)
|
||||||
|
return try_connection(url, retries-1, period)
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
|
||||||
|
def get_status(url):
|
||||||
|
return requests.get(url).json()['status']
|
||||||
|
|
||||||
|
|
||||||
def babysit_builder(request):
|
def babysit_builder(request):
|
||||||
manager = digitalocean.Manager(client_id=app.config['DO_CLIENT_ID'],
|
manager = digitalocean.Manager(client_id=app.config['DO_CLIENT_ID'],
|
||||||
api_key=app.config['DO_CLIENT_SECRET'])
|
api_key=app.config['DO_CLIENT_SECRET'])
|
||||||
|
@ -60,16 +77,62 @@ def babysit_builder(request):
|
||||||
repository_build.phase = 'initializing'
|
repository_build.phase = 'initializing'
|
||||||
repository_build.save()
|
repository_build.save()
|
||||||
|
|
||||||
|
ssh_client = paramiko.SSHClient()
|
||||||
|
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh_client.connect(self._container_ip, self._config.sshd_port, "root",
|
||||||
|
look_for_keys=False,
|
||||||
|
key_filename=app.config['DO_SSH_PRIVATE_KEY_FILENAME'])
|
||||||
|
|
||||||
# tell it to pull and run the buildserver
|
# Pull and run the buildserver
|
||||||
|
pull_cmd = 'docker pull quay.io/quay/buildserver'
|
||||||
|
_, stdout, _ = ssh_client.exec_command(pull_cmd)
|
||||||
|
|
||||||
|
start_cmd = 'sudo docker run -d -privileged quay.io/quay/buildserver'
|
||||||
|
_, stdout, _ = ssh_client.exec_command(start_cmd)
|
||||||
|
|
||||||
# wait for the server to be ready
|
# wait for the server to be ready
|
||||||
|
logger.debug('Waiting for buildserver to be ready')
|
||||||
|
build_endpoint = 'http://%s:5002/build/' % droplet.ip_address
|
||||||
|
try:
|
||||||
|
try_connection()
|
||||||
|
except ConnectionError:
|
||||||
|
#TODO cleanup
|
||||||
|
pass
|
||||||
|
|
||||||
# send it the job
|
# send it the job
|
||||||
|
logger.debug('Sending build server request')
|
||||||
|
|
||||||
|
user_files = UserRequestFiles(app.config['AWS_ACCESS_KEY'],
|
||||||
|
app.config['AWS_SECRET_KEY'],
|
||||||
|
app.config['REGISTRY_S3_BUCKET'])
|
||||||
|
|
||||||
|
repo = repository_build.repository
|
||||||
|
payload = {
|
||||||
|
'tag': 'quay.io/%s/%s' % (repo.namespace, repo.name),
|
||||||
|
'resource_url': user_files.get_file_url(repository_build.resource_key),
|
||||||
|
}
|
||||||
|
start_build = requests.post(build_endpoint, data=payload)
|
||||||
|
|
||||||
# wait for the job to be complete
|
# wait for the job to be complete
|
||||||
|
status_url = start_build.headers['Location']
|
||||||
|
|
||||||
|
logger.debug('Waiting for job to be complete')
|
||||||
|
status = get_status(status_url)
|
||||||
|
while status != 'error' and status != 'completed':
|
||||||
|
logger.debug('Job status is: %s' % status)
|
||||||
|
time.sleep(5)
|
||||||
|
status = get_status(status_url)
|
||||||
|
|
||||||
|
logger.debug('Job complete with status: %s' % status)
|
||||||
|
if status == 'error':
|
||||||
|
repository_build.phase = 'error'
|
||||||
|
else:
|
||||||
|
repository_build.phase = 'completed'
|
||||||
|
repository_build.save()
|
||||||
|
|
||||||
# clean up the DO node
|
# clean up the DO node
|
||||||
|
logger.debug('Cleaning up DO node.')
|
||||||
|
droplet.destroy()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -91,7 +154,8 @@ def process_work_items(pool):
|
||||||
dockerfile_build_queue.complete(local_item)
|
dockerfile_build_queue.complete(local_item)
|
||||||
return complete_callback
|
return complete_callback
|
||||||
|
|
||||||
pool.apply_async(babysit_builder, [request], callback=build_callback(item))
|
pool.apply_async(babysit_builder, [request],
|
||||||
|
callback=build_callback(item))
|
||||||
|
|
||||||
item = dockerfile_build_queue.get()
|
item = dockerfile_build_queue.get()
|
||||||
|
|
||||||
|
|
Reference in a new issue