Merge remote-tracking branch 'origin/master' into rustedbuilds

This commit is contained in:
jakedt 2014-02-17 16:40:28 -05:00
commit fc4983ed8b
75 changed files with 4280 additions and 700 deletions

View file

@ -36,7 +36,6 @@ start the workers:
```
STACK=prod python -m workers.diffsworker -D
STACK=prod python -m workers.dockerfilebuild -D
STACK=prod python -m workers.webhookworker -D
```

View file

@ -3,7 +3,6 @@ import logging
from app import app as application
from data.model import db as model_db
# Initialize logging
application.config['LOGGING_CONFIG']()
@ -16,6 +15,7 @@ from endpoints.web import web
from endpoints.tags import tags
from endpoints.registry import registry
from endpoints.webhooks import webhooks
from endpoints.realtime import realtime
logger = logging.getLogger(__name__)
@ -26,6 +26,7 @@ application.register_blueprint(tags, url_prefix='/v1')
application.register_blueprint(registry, url_prefix='/v1')
application.register_blueprint(api, url_prefix='/api')
application.register_blueprint(webhooks, url_prefix='/webhooks')
application.register_blueprint(realtime, url_prefix='/realtime')
def close_db(exc):

View file

@ -29,7 +29,7 @@ def process_basic_auth(auth):
logger.debug('Invalid basic auth format.')
return
credentials = b64decode(normalized[1]).split(':')
credentials = b64decode(normalized[1]).split(':', 1)
if len(credentials) != 2:
logger.debug('Invalid basic auth credential format.')

View file

@ -6,6 +6,7 @@ from storage.s3 import S3Storage
from storage.local import LocalStorage
from data.userfiles import UserRequestFiles
from data.buildlogs import BuildLogs
from data.userevent import UserEventBuilder
from util import analytics
from test.teststorage import FakeStorage, FakeUserfiles
@ -15,7 +16,7 @@ from test.testlogs import TestBuildLogs
class FlaskConfig(object):
SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
JSONIFY_PRETTYPRINT_REGULAR = False
class FlaskProdConfig(FlaskConfig):
SESSION_COOKIE_SECURE = True
@ -91,6 +92,10 @@ class RedisBuildLogs(object):
BUILDLOGS = BuildLogs('logs.quay.io')
class UserEventConfig(object):
USER_EVENTS = UserEventBuilder('logs.quay.io')
class TestBuildLogs(object):
BUILDLOGS = TestBuildLogs('logs.quay.io', 'devtable', 'building',
'deadbeef-dead-beef-dead-beefdeadbeef')
@ -159,7 +164,8 @@ def logs_init_builder(level=logging.DEBUG,
class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles,
FakeAnalytics, StripeTestConfig, RedisBuildLogs):
FakeAnalytics, StripeTestConfig, RedisBuildLogs,
UserEventConfig):
LOGGING_CONFIG = logs_init_builder(logging.WARN)
POPULATE_DB_TEST_DATA = True
TESTING = True
@ -168,7 +174,7 @@ class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles,
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig,
DigitalOceanConfig, BuildNodeConfig, S3Userfiles,
TestBuildLogs):
UserEventConfig, TestBuildLogs):
LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter())
SEND_FILE_MAX_AGE_DEFAULT = 0
POPULATE_DB_TEST_DATA = True
@ -177,7 +183,8 @@ class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
StripeLiveConfig, MixpanelTestConfig,
GitHubProdConfig, DigitalOceanConfig,
BuildNodeConfig, S3Userfiles, RedisBuildLogs):
BuildNodeConfig, S3Userfiles, RedisBuildLogs,
UserEventConfig):
LOGGING_CONFIG = logs_init_builder()
SEND_FILE_MAX_AGE_DEFAULT = 0
@ -185,7 +192,7 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
class ProductionConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL,
StripeLiveConfig, MixpanelProdConfig,
GitHubProdConfig, DigitalOceanConfig, BuildNodeConfig,
S3Userfiles, RedisBuildLogs):
S3Userfiles, RedisBuildLogs, UserEventConfig):
LOGGING_CONFIG = logs_init_builder()
SEND_FILE_MAX_AGE_DEFAULT = 0

View file

@ -3,6 +3,10 @@ import json
class BuildLogs(object):
ERROR = 'error'
COMMAND = 'command'
PHASE = 'phase'
def __init__(self, redis_host):
self._redis = redis.StrictRedis(host=redis_host)
@ -10,10 +14,6 @@ class BuildLogs(object):
def _logs_key(build_id):
return 'builds/%s/logs' % build_id
@staticmethod
def _commands_key(build_id):
return 'builds/%s/commands' % build_id
def append_log_entry(self, build_id, log_obj):
"""
Appends the serialized form of log_obj to the end of the log entry list
@ -21,7 +21,7 @@ class BuildLogs(object):
"""
return self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj))
def append_log_message(self, build_id, log_message):
def append_log_message(self, build_id, log_message, log_type=None):
"""
Wraps the message in an envelope and push it to the end of the log entry
list and returns the index at which it was inserted.
@ -29,54 +29,21 @@ class BuildLogs(object):
log_obj = {
'message': log_message
}
if log_type:
log_obj['type'] = log_type
return self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj)) - 1
def append_command_message(self, build_id, command_message):
"""
Wraps the message in an envelope and push it to the end of the log entry
list, to the commands list, and returns the new length of the list.
"""
log_obj = {
'message': command_message,
'is_command': True,
}
idx = self._redis.rpush(self._logs_key(build_id), json.dumps(log_obj)) - 1
cmd_obj = {
'message': command_message,
'index': idx,
}
self._redis.rpush(self._commands_key(build_id), json.dumps(cmd_obj))
return idx
def get_log_entries(self, build_id, start_index, end_index):
def get_log_entries(self, build_id, start_index):
"""
Returns a tuple of the current length of the list and an iterable of the
requested log entries. End index is inclusive.
requested log entries.
"""
llen = self._redis.llen(self._logs_key(build_id))
log_entries = self._redis.lrange(self._logs_key(build_id), start_index,
end_index)
log_entries = self._redis.lrange(self._logs_key(build_id), start_index, -1)
return (llen, (json.loads(entry) for entry in log_entries))
def get_commands(self, build_id):
"""
Returns a list of all Dockerfile commands that have passed through the
specified build thus far.
"""
commands = self._redis.lrange(self._commands_key(build_id), 0, -1)
return (json.loads(cmd) for cmd in commands)
def get_last_command(self, build_id):
"""
Returns only the last command from the list of commands.
"""
commands = self._redis.lrange(self._commands_key(build_id), -1, -1)
if commands:
return json.loads(commands[-1])
else:
return None
@staticmethod
def _status_key(build_id):
return 'builds/%s/status' % build_id

View file

@ -216,7 +216,7 @@ class RepositoryBuild(BaseModel):
tag = CharField()
phase = CharField(default='waiting')
started = DateTimeField(default=datetime.now)
display_name = CharField()
class QueueItem(BaseModel):

View file

@ -109,7 +109,10 @@ def create_organization(name, email, creating_user):
return new_org
except InvalidUsernameException:
raise InvalidOrganizationException('Invalid organization name: %s' % name)
msg = ('Invalid organization name: %s Organization names must consist ' +
'solely of lower case letters, numbers, and underscores. ' +
'[a-z0-9_]') % name
raise InvalidOrganizationException(msg)
def create_robot(robot_shortname, parent):
@ -760,6 +763,16 @@ def get_all_repo_users(namespace_name, repository_name):
Repository.name == repository_name)
def get_repository_for_resource(resource_key):
joined = Repository.select().join(RepositoryBuild)
query = joined.where(RepositoryBuild.resource_key == resource_key).limit(1)
result = list(query)
if not result:
return None
return result[0]
def get_repository(namespace_name, repository_name):
try:
return Repository.get(Repository.name == repository_name,
@ -970,6 +983,41 @@ def delete_tag_and_images(namespace_name, repository_name, tag_name):
store.remove(repository_path)
def garbage_collect_repository(namespace_name, repository_name):
# Get a list of all images used by tags in the repository
tag_query = (RepositoryTag
.select(RepositoryTag, Image)
.join(Image)
.switch(RepositoryTag)
.join(Repository)
.where(Repository.name == repository_name,
Repository.namespace == namespace_name))
referenced_anscestors = set()
for tag in tag_query:
# The anscestor list is in the format '/1/2/3/', extract just the ids
anscestor_id_strings = tag.image.ancestors.split('/')[1:-1]
ancestor_list = [int(img_id_str) for img_id_str in anscestor_id_strings]
referenced_anscestors = referenced_anscestors.union(set(ancestor_list))
referenced_anscestors.add(tag.image.id)
all_repo_images = get_repository_images(namespace_name, repository_name)
all_images = {int(img.id):img for img in all_repo_images}
to_remove = set(all_images.keys()).difference(referenced_anscestors)
logger.info('Cleaning up unreferenced images: %s', to_remove)
for image_id_to_remove in to_remove:
image_to_remove = all_images[image_id_to_remove]
image_path = store.image_path(namespace_name, repository_name,
image_to_remove.docker_image_id)
image_to_remove.delete_instance()
logger.debug('Deleting image storage: %s' % image_path)
store.remove(image_path)
return len(to_remove)
def get_tag_image(namespace_name, repository_name, tag_name):
joined = Image.select().join(RepositoryTag).join(Repository)
fetched = list(joined.where(Repository.name == repository_name,
@ -1021,7 +1069,8 @@ def create_or_update_tag(namespace_name, repository_name, tag_name,
(namespace_name, repository_name))
try:
image = Image.get(Image.docker_image_id == tag_docker_image_id)
image = Image.get(Image.docker_image_id == tag_docker_image_id,
Image.repository == repo)
except Image.DoesNotExist:
raise DataModelException('Invalid image with id: %s' %
tag_docker_image_id)
@ -1270,9 +1319,11 @@ def list_repository_builds(namespace_name, repository_name,
return fetched
def create_repository_build(repo, access_token, resource_key, tag):
def create_repository_build(repo, access_token, resource_key, tag,
display_name):
return RepositoryBuild.create(repository=repo, access_token=access_token,
resource_key=resource_key, tag=tag)
resource_key=resource_key, tag=tag,
display_name=display_name)
def create_webhook(repo, params_obj):

View file

@ -64,5 +64,5 @@ class WorkQueue(object):
image_diff_queue = WorkQueue('imagediff')
dockerfile_build_queue = WorkQueue('dockerfilebuild')
dockerfile_build_queue = WorkQueue('dockerfilebuild2')
webhook_queue = WorkQueue('webhook')

90
data/userevent.py Normal file
View file

@ -0,0 +1,90 @@
import redis
import json
import threading
class UserEventBuilder(object):
"""
Defines a helper class for constructing UserEvent and UserEventListener
instances.
"""
def __init__(self, redis_host):
self._redis_host = redis_host
def get_event(self, username):
return UserEvent(self._redis_host, username)
def get_listener(self, username, events):
return UserEventListener(self._redis_host, username, events)
class UserEvent(object):
"""
Defines a helper class for publishing to realtime user events
as backed by Redis.
"""
def __init__(self, redis_host, username):
self._redis = redis.StrictRedis(host=redis_host)
self._username = username
@staticmethod
def _user_event_key(username, event_id):
return 'user/%s/events/%s' % (username, event_id)
def publish_event_data_sync(self, event_id, data_obj):
return self._redis.publish(self._user_event_key(self._username, event_id), json.dumps(data_obj))
def publish_event_data(self, event_id, data_obj):
"""
Publishes the serialized form of the data object for the given event. Note that this occurs
in a thread to prevent blocking.
"""
def conduct():
try:
self.publish_event_data_sync(event_id, data_obj)
except Exception as e:
print e
thread = threading.Thread(target=conduct)
thread.start()
class UserEventListener(object):
"""
Defines a helper class for subscribing to realtime user events as
backed by Redis.
"""
def __init__(self, redis_host, username, events=set([])):
channels = [self._user_event_key(username, e) for e in events]
self._redis = redis.StrictRedis(host=redis_host)
self._pubsub = self._redis.pubsub()
self._pubsub.subscribe(channels)
@staticmethod
def _user_event_key(username, event_id):
return 'user/%s/events/%s' % (username, event_id)
def event_stream(self):
"""
Starts listening for events on the channel(s), yielding for each event
found.
"""
for item in self._pubsub.listen():
channel = item['channel']
event_id = channel.split('/')[3] # user/{username}/{events}/{id}
data = None
try:
data = json.loads(item['data'] or '{}')
except:
pass
if data:
yield event_id, data
def stop(self):
"""
Unsubscribes from the channel(s). Should be called once the connection
has terminated.
"""
self._pubsub.unsubscribe()

View file

@ -59,3 +59,9 @@ class UserRequestFiles(object):
full_key = os.path.join(self._prefix, file_id)
k = Key(self._bucket, full_key)
return k.generate_url(expires_in)
def get_file_checksum(self, file_id):
self._initialize_s3()
full_key = os.path.join(self._prefix, file_id)
k = self._bucket.lookup(full_key)
return k.etag[1:-1][:7]

View file

@ -262,7 +262,6 @@ def convert_user_to_organization():
@internal_api_call
def change_user_details():
user = current_user.db_user()
user_data = request.get_json()
try:
@ -315,6 +314,8 @@ def create_new_user():
@internal_api_call
def signin_user():
signin_data = request.get_json()
if not signin_data:
abort(404)
username = signin_data['username']
password = signin_data['password']
@ -421,6 +422,7 @@ def get_matching_entities(prefix):
team_data = [entity_team_view(team) for team in teams]
user_data = [user_view(user) for user in users]
return jsonify({
'results': team_data + user_data
})
@ -446,11 +448,16 @@ def create_organization():
existing = None
try:
existing = (model.get_organization(org_data['name']) or
model.get_user(org_data['name']))
existing = model.get_organization(org_data['name'])
except model.InvalidOrganizationException:
pass
if not existing:
try:
existing = model.get_user(org_data['name'])
except model.InvalidUserException:
pass
if existing:
msg = 'A user or organization with this name already exists'
return request_error(message=msg)
@ -604,9 +611,9 @@ def create_organization_prototype_permission(orgname):
'name' in details['activating_user']):
activating_username = details['activating_user']['name']
delegate = details['delegate']
delegate_kind = delegate['kind']
delegate_name = delegate['name']
delegate = details['delegate'] if 'delegate' in details else {}
delegate_kind = delegate.get('kind', None)
delegate_name = delegate.get('name', None)
delegate_username = delegate_name if delegate_kind == 'user' else None
delegate_teamname = delegate_name if delegate_kind == 'team' else None
@ -622,7 +629,7 @@ def create_organization_prototype_permission(orgname):
return request_error(message='Unknown activating user')
if not delegate_user and not delegate_team:
return request_error(message='Missing delagate user or team')
return request_error(message='Missing delegate user or team')
role_name = details['role']
@ -1148,7 +1155,9 @@ def build_status_view(build_obj):
'id': build_obj.uuid,
'phase': build_obj.phase,
'started': build_obj.started,
'display_name': build_obj.display_name,
'status': status,
'resource_key': build_obj.resource_key
}
@ -1189,38 +1198,12 @@ def get_repo_build_logs(namespace, repository, build_uuid):
build = model.get_repository_build(namespace, repository, build_uuid)
start_param = request.args.get('start', None)
end = int(request.args.get('end', -1))
start = int(request.args.get('start', 0))
last_command = None
include_commands = request.args.get('commands', 'false')
if include_commands.lower() not in {'0', 'false'}:
commands = [cmd for cmd in build_logs.get_commands(build.uuid)]
response_obj['commands'] = commands
if commands:
last_command = commands[-1]
elif start_param is None:
last_command = build_logs.get_last_command(build.uuid)
if start_param is None:
if last_command:
start = last_command['index']
else:
start = 0
else:
start = int(start_param)
count, logs = build_logs.get_log_entries(build.uuid, start, end)
if start < 0:
start = max(0, count + start)
if end < 0:
end = count + end
count, logs = build_logs.get_log_entries(build.uuid, start)
response_obj.update({
'start': start,
'end': end,
'total': count,
'logs': [log for log in logs],
})
@ -1239,18 +1222,28 @@ def request_repo_build(namespace, repository):
logger.debug('User requested repository initialization.')
dockerfile_id = request.get_json()['file_id']
# Check if the dockerfile resource has already been used. If so, then it can only be reused if the
# user has access to the repository for which it was used.
associated_repository = model.get_repository_for_resource(dockerfile_id)
if associated_repository:
if not ModifyRepositoryPermission(associated_repository.namespace, associated_repository.name):
abort(403)
# Start the build.
repo = model.get_repository(namespace, repository)
token = model.create_access_token(repo, 'write')
display_name = user_files.get_file_checksum(dockerfile_id)
logger.debug('**********Md5: %s' % display_name)
host = urlparse.urlparse(request.url).netloc
tag = '%s/%s/%s' % (host, repo.namespace, repo.name)
build_request = model.create_repository_build(repo, token, dockerfile_id,
tag)
tag, display_name)
dockerfile_build_queue.put(json.dumps({
'build_uuid': build_request.uuid,
'namespace': namespace,
'repository': repository,
}))
}), retries_remaining=1)
log_action('build_dockerfile', namespace,
{'repo': repository, 'namespace': namespace,
@ -1302,7 +1295,11 @@ def create_webhook(namespace, repository):
def get_webhook(namespace, repository, public_id):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
webhook = model.get_webhook(namespace, repository, public_id)
try:
webhook = model.get_webhook(namespace, repository, public_id)
except model.InvalidWebhookException:
abort(404)
return jsonify(webhook_view(webhook))
abort(403) # Permission denied
@ -1697,7 +1694,11 @@ def list_repo_tokens(namespace, repository):
def get_tokens(namespace, repository, code):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
perm = model.get_repo_delegate_token(namespace, repository, code)
try:
perm = model.get_repo_delegate_token(namespace, repository, code)
except model.InvalidTokenException:
abort(404)
return jsonify(token_view(perm))
abort(403) # Permission denied
@ -1834,6 +1835,8 @@ def set_card(user, token):
cus.save()
except stripe.CardError as e:
return carderror_response(e)
except stripe.InvalidRequestError as e:
return carderror_response(e)
return get_card(user)

View file

@ -5,9 +5,9 @@ import urlparse
from flask import request, make_response, jsonify, session, Blueprint
from functools import wraps
from data import model
from data import model, userevent
from data.queue import webhook_queue
from app import mixpanel
from app import mixpanel, app
from auth.auth import (process_auth, get_authenticated_user,
get_validated_token)
from util.names import parse_repository_name
@ -80,8 +80,16 @@ def create_user():
if existing_user:
verified = model.verify_user(username, password)
if verified:
# Mark that the user was logged in.
event = app.config['USER_EVENTS'].get_event(username)
event.publish_event_data('docker-cli', {'action': 'login'})
return make_response('Verified', 201)
else:
# Mark that the login failed.
event = app.config['USER_EVENTS'].get_event(username)
event.publish_event_data('docker-cli', {'action': 'loginfailure'})
abort(400, 'Invalid password.', issue='login-failure')
else:
@ -186,9 +194,21 @@ def create_repository(namespace, repository):
}
if get_authenticated_user():
mixpanel.track(get_authenticated_user().username, 'push_repo',
extra_params)
metadata['username'] = get_authenticated_user().username
username = get_authenticated_user().username
mixpanel.track(username, 'push_repo', extra_params)
metadata['username'] = username
# Mark that the user has started pushing the repo.
user_data = {
'action': 'push_repo',
'repository': repository,
'namespace': namespace
}
event = app.config['USER_EVENTS'].get_event(username)
event.publish_event_data('docker-cli', user_data)
else:
mixpanel.track(get_validated_token().code, 'push_repo', extra_params)
metadata['token'] = get_validated_token().friendly_name
@ -222,6 +242,21 @@ def update_images(namespace, repository):
updated_tags[image['Tag']] = image['id']
model.set_image_checksum(image['id'], repo, image['checksum'])
if get_authenticated_user():
username = get_authenticated_user().username
# Mark that the user has pushed the repo.
user_data = {
'action': 'pushed_repo',
'repository': repository,
'namespace': namespace
}
event = app.config['USER_EVENTS'].get_event(username)
event.publish_event_data('docker-cli', user_data)
num_removed = model.garbage_collect_repository(namespace, repository)
# Generate a job for each webhook that has been added to this repo
webhooks = model.list_webhooks(namespace, repository)
for webhook in webhooks:
@ -238,6 +273,7 @@ def update_images(namespace, repository):
'visibility': repo.visibility.name,
'updated_tags': updated_tags,
'pushed_image_count': len(image_with_checksums),
'pruned_image_count': num_removed,
}
webhook_queue.put(json.dumps(webhook_data))

81
endpoints/realtime.py Normal file
View file

@ -0,0 +1,81 @@
import logging
import redis
import json
from functools import wraps
from flask import request, make_response, Blueprint, abort, Response
from flask.ext.login import current_user, logout_user
from data import model, userevent
from app import app
logger = logging.getLogger(__name__)
realtime = Blueprint('realtime', __name__)
def api_login_required(f):
@wraps(f)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
abort(401)
if (current_user and current_user.db_user() and
current_user.db_user().organization):
abort(401)
if (current_user and current_user.db_user() and
current_user.db_user().robot):
abort(401)
return f(*args, **kwargs)
return decorated_view
@realtime.route("/user/")
@api_login_required
def index():
debug_template = """
<html>
<head>
</head>
<body>
<h1>Server sent events</h1>
<div id="event"></div>
<script type="text/javascript">
var eventOutputContainer = document.getElementById("event");
var evtSrc = new EventSource("/realtime/user/subscribe?events=docker-cli");
evtSrc.onmessage = function(e) {
console.log(e.data);
eventOutputContainer.innerHTML = e.data;
};
</script>
</body>
</html>
"""
return(debug_template)
@realtime.route("/user/test")
@api_login_required
def user_test():
evt = userevent.UserEvent('logs.quay.io', current_user.db_user().username)
evt.publish_event_data('test', {'foo': 2})
return 'OK'
@realtime.route("/user/subscribe")
@api_login_required
def user_subscribe():
def wrapper(listener):
for event_id, data in listener.event_stream():
message = {'event': event_id, 'data': data}
json_string = json.dumps(message)
yield 'data: %s\n\n' % json_string
events = request.args.get('events', '').split(',')
if not events:
abort(404)
listener = userevent.UserEventListener('logs.quay.io', current_user.db_user().username, events)
return Response(wrapper(listener), mimetype="text/event-stream")

View file

@ -73,22 +73,8 @@ def delete_tag(namespace, repository, tag):
if permission.can():
model.delete_tag(namespace, repository, tag)
model.garbage_collect_repository(namespace, repository)
return make_response('Deleted', 204)
abort(403)
@tags.route('/repositories/<path:repository>/tags',
methods=['DELETE'])
@process_auth
@parse_repository_name
def delete_repository_tags(namespace, repository):
permission = ModifyRepositoryPermission(namespace, repository)
if permission.can():
model.delete_all_repository_tags(namespace, repository)
return make_response('Deleted', 204)
return make_response('Deleted', 200)
abort(403)

View file

@ -63,6 +63,12 @@ def guide():
return index('')
@web.route('/tutorial/')
@no_cache
def tutorial():
return index('')
@web.route('/organizations/')
@web.route('/organizations/new/')
@no_cache
@ -88,6 +94,12 @@ def contact():
return index('')
@web.route('/about/')
@no_cache
def about():
return index('')
@web.route('/new/')
@no_cache
def new():

View file

@ -277,7 +277,8 @@ def populate_database():
token = model.create_access_token(building, 'write')
tag = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name)
build = model.create_repository_build(building, token, '123-45-6789', tag)
build = model.create_repository_build(building, token, '123-45-6789', tag,
'build-name')
build.uuid = 'deadbeef-dead-beef-dead-beefdeadbeef'
build.save()

1
run-local.sh Executable file
View file

@ -0,0 +1 @@
gunicorn -c conf/gunicorn_local.py application:application

View file

@ -543,6 +543,10 @@ i.toggle-icon:hover {
margin-right: 6px;
}
.active .build-tab-link .phase-icon {
box-shadow: 0px 0px 10px #FFFFFF, 0px 0px 10px #FFFFFF;
}
.build-status .phase-icon {
margin-top: 4px;
float: left;
@ -842,10 +846,20 @@ i.toggle-icon:hover {
display: none;
}
.visible-md-inline {
display: none;
}
.hidden-sm-inline {
display: inline;
}
@media (min-width: 991px) {
.visible-md-inline {
display: inline;
}
}
@media (max-width: 991px) and (min-width: 768px) {
.visible-sm-inline {
display: inline;
@ -1445,7 +1459,10 @@ p.editable:hover i {
}
.repo .header {
margin-bottom: 10px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
position: relative;
}
@ -1495,36 +1512,12 @@ p.editable:hover i {
display: inline-block;
}
.repo .status-boxes {
float: right;
margin-bottom: 20px;
}
.repo .status-boxes .status-box {
cursor: pointer;
.repo .repo-controls .count {
display: inline-block;
border: 1px solid #eee;
border-radius: 4px;
}
.repo .status-boxes .status-box .title {
padding: 4px;
display: inline-block;
padding-left: 10px;
padding-right: 10px;
}
.repo .status-boxes .status-box .title i {
margin-right: 6px;
}
.repo .status-boxes .status-box .count {
display: inline-block;
background-image: linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);
padding: 4px;
padding-left: 10px;
padding-right: 10px;
padding-left: 4px;
padding-right: 4px;
font-weight: bold;
color: #428bca;
transform: scaleX(0);
-webkit-transform: scaleX(0);
@ -1535,13 +1528,13 @@ p.editable:hover i {
-moz-transition: -moz-transform 500ms ease-in-out;
}
.repo .status-boxes .status-box .count.visible {
.repo .repo-controls .count.visible {
transform: scaleX(1);
-webkit-transform: scaleX(1);
-moz-transform: scaleX(1);
}
.repo .pull-command {
.repo .repo-controls {
float: right;
display: inline-block;
font-size: 0.8em;
@ -1745,69 +1738,114 @@ p.editable:hover i {
overflow: auto;
}
.repo-build .build-pane .build-logs .command-logs {
margin: 10px;
padding-right: 10px;
.repo-build .build-pane .build-logs .container-header {
padding: 2px;
}
.repo-build .build-pane .build-logs .command-entry,
.repo-build .build-pane .build-logs .log-entry {
.repo-build .build-pane .build-logs .container-logs {
margin: 4px;
padding-bottom: 4px;
}
.repo-build .build-pane .build-logs .command-title,
.repo-build .build-pane .build-logs .log-entry .message,
.repo-build .build-pane .build-logs .log-entry .message span {
font-family: Consolas, "Lucida Console", Monaco, monospace;
font-size: 13px;
}
.repo-build .build-pane .build-logs .command-entry {
.repo-build .build-pane .build-logs .container-header {
cursor: pointer;
position: relative;
}
.repo-build .build-pane .build-logs .command-entry i.fa.chevron {
.repo-build .build-pane .build-logs .container-header i.fa.chevron {
color: #666;
margin-right: 4px;
width: 14px;
text-align: center;
position: absolute;
top: 2px;
top: 6px;
left: 0px;
}
.repo-build .build-pane .build-logs .command-entry .label {
text-align: center;
.repo-build .build-pane .build-logs .log-container.command {
margin-left: 42px;
}
.repo-build .build-pane .build-logs .container-header.building {
margin-bottom: 10px;
}
.repo-build .build-pane .build-logs .container-header.pushing {
margin-top: 10px;
}
.repo-build .build-log-error-element {
position: relative;
display: inline-block;
margin: 10px;
padding: 10px;
background: rgba(255, 0, 0, 0.17);
border-radius: 10px;
margin-left: 22px;
}
.repo-build .build-log-error-element i.fa {
color: red;
position: absolute;
top: 13px;
left: 11px;
}
.repo-build .build-log-error-element .error-message {
display: inline-block;
margin-left: 25px;
}
.repo-build .build-pane .build-logs .container-header .label {
padding-top: 4px;
text-align: right;
margin-right: 4px;
vertical-align: middle;
width: 86px;
display: inline-block;
background-color: #aaa;
border-right: 4px solid #aaa;
background-color: #444;
position: absolute;
top: 2px;
top: 4px;
left: 24px;
}
.repo-build .build-pane .build-logs .command-entry .command-title {
.repo-build .build-pane .build-logs .container-header .container-content {
display: block;
padding-left: 20px;
}
.repo-build .build-pane .build-logs .container-header .container-content.build-log-command {
padding-left: 120px;
}
.label.FROM {
background-color: #5bc0de !important;
border-color: #5bc0de !important;
}
.label.CMD, .label.EXPOSE, .label.ENTRYPOINT {
background-color: #428bca !important;
border-color: #428bca !important;
}
.label.RUN, .label.ADD {
background-color: #5cb85c !important;
border-color: #5cb85c !important;
}
.label.ENV, .label.VOLUME, .label.USER, .label.WORKDIR {
background-color: #f0ad4e !important;
border-color: #f0ad4e !important;
}
.label.MAINTAINER {
background-color: #aaa !important;
border-color: #aaa !important;
}
.repo-build .build-pane .build-logs .log-entry {
@ -2675,13 +2713,17 @@ p.editable:hover i {
color: #888 !important;
}
.repo-breadcrumb-element .current a {
color: #444 !important;
}
.repo-breadcrumb-element .crumb:after {
content: "/";
color: #ccc;
margin-left: 4px;
}
.repo-breadcrumb-element .crumb:hover, .repo-breadcrumb-element .crumb:hover a {
.repo-breadcrumb-element .crumb:hover, .repo-breadcrumb-element .crumb:hover a, .repo-breadcrumb-element .current:hover a {
color: #2a6496 !important;
text-decoration: none;
}
@ -2913,3 +2955,240 @@ p.editable:hover i {
.contact-options {
margin-top: 60px;
}
/*********************************************/
.angular-tour-ui-element.overlay {
display: block;
position: fixed;
bottom: 50px;
right: 20px;
border-radius: 10px;
z-index: 9999999;
background: white;
-webkit-box-shadow: 0 5px 15px rgba(0,0,0,0.5);
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
opacity: 0;
transition: opacity 750ms ease-in-out;
-webkit-transition: opacity 750ms ease-in-out;
min-width: 400px;
}
.angular-tour-ui-element.overlay.touring {
opacity: 1;
}
.angular-tour-ui-element.overlay.nottouring {
pointer-events: none;
position: absolute;
left: -10000px;
width: 0px;
height: 0px;
}
.angular-tour-ui-element.overlay .tour-title {
background-color: #3276b1;
color: white;
padding: 4px;
padding-left: 6px;
padding-right: 6px;
border-radius: 4px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.angular-tour-ui-element.overlay .tour-title h4 {
display: inline-block;
font-size: 16px;
margin: 0px;
padding: 2px;
}
.angular-tour-ui-element.overlay .step-title {
font-size: 20px;
}
.angular-tour-ui-element.overlay .step-content {
padding: 10px;
padding-left: 0px;
font-size: 16px;
}
.angular-tour-ui-element.overlay .tour-contents {
padding: 10px;
}
.angular-tour-ui-element.overlay .controls {
text-align: right;
}
.angular-tour-ui-element.overlay .controls .btn {
font-size: 16px;
}
.angular-tour-ui-element.overlay .fa {
display: none;
}
/**************************************************/
.angular-tour-ui-element.inline {
}
.angular-tour-ui-element.inline .fa-dot-circle-o {
font-size: 34px;
background: #ddd;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
padding-top: 4px;
vertical-align: middle;
margin-right: 10px;
}
.angular-tour-ui-element.inline .tour-title h4 {
font-size: 28px;
padding-bottom: 10px;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
}
.angular-tour-ui-element.inline .tour-title .close {
display: none;
}
.angular-tour-ui-element.inline .step-title {
font-size: 20px;
margin-bottom: 20px;
}
.angular-tour-ui-element.inline .step-content {
margin-bottom: 10px;
}
.angular-tour-ui-element.inline .controls {
margin-top: 20px;
border-top: 1px solid #eee;
padding-top: 10px;
}
.angular-tour-ui-element p {
margin-bottom: 20px;
}
.angular-tour-ui-element .wait-message {
font-size: 16px;
color: #999;
}
.angular-tour-ui-element .wait-message .quay-spinner {
display: inline-block;
vertical-align: middle;
margin-right: 6px;
}
.angular-tour-ui-element .wait-message .quay-spinner .small-spinner {
border-top-color: #999;
border-left-color: #999;
}
.angular-tour-ui-element .note {
margin: 10px;
padding: 6px;
background: #eee;
border: 1px solid #ddd;
}
.angular-tour-ui-element .skip-message {
display: inline-block;
margin-left: 20px;
}
pre.command {
padding: 20px;
background: #fff;
text-shadow: none;
overflow: auto;
border: solid 1px #ccc;
position: relative;
margin-top: 10px;
margin-bottom: 20px;
}
pre.command:before {
content: "\f120";
font-family: "FontAwesome";
font-size: 16px;
margin-right: 6px;
color: #999;
}
.form-inline {
display: inline-block;
margin-left: 10px;
}
/*********************************************/
.contact-options .option-twitter .fa-circle {
color: #00b0ed;
}
.contact-options .option-phone .fa-circle {
color: #1dd924;
}
.contact-options .option-irc .fa-circle {
color: #e52f00;
}
.contact-options .option-email .fa-circle {
color: #1b72f1;
}
.about-us .row {
margin-bottom: 30px;
}
.about-basic-icon {
display: inline-block;
margin-right: 10px;
color: #555;
}
.about-basic-text {
display: inline-block;
}
.repo-name-small:before {
content: "\f0a0";
font-family: FontAwesome;
font-size: 20px;
margin-right: 6px;
vertical-align: middle;
font-weight: normal;
}
.repo-name-small {
background: none repeat scroll 0 0 #D9EDF7;
border-radius: 10px;
margin-left: 6px;
margin-right: 6px;
display: inline-block;
font-size: 14px;
font-weight: bold;
padding: 4px 8px;
}
.file-drop {
padding: 10px;
margin: 10px;
}

View file

@ -0,0 +1,28 @@
<div class="angular-tour-ui-element"
ng-class="[tour ? 'touring' : 'nottouring', inline ? 'inline' : 'overlay']">
<div class="tour-title">
<h4><i class="fa fa-dot-circle-o"></i> {{ tour.title }}</h4>
<button type="button" class="close" ng-click="stop()" aria-hidden="true">&times;</button>
</div>
<div class="tour-contents">
<div class="step-title" ng-shpw="step.title">{{ step.title }}</div>
<div class="slide-animate-container">
<div class="step-content" ng-show="step.content" ng-bind-html="step.content || ''"></div>
<div class="step-content">
<ng:include src="step.templateUrl" scope="info"></ng:include>
</div>
</div>
<div class="controls">
<button class="btn btn-primary" ng-click="next()" ng-show="hasNextStep && !step.signal">Next</button>
<button class="btn btn-primary" ng-click="stop()" ng-show="!hasNextStep && !inline">Done</button>
</div>
<div class="wait-message" ng-show="step.waitMessage">
<div class="quay-spinner"></div> {{ step.waitMessage }}
<span class="skip-message" ng-show="step.skipTitle"><button class="btn btn-default" ng-click="next()">{{ step.skipTitle }}</button></span>
</div>
</div>
</div>

View file

@ -0,0 +1,6 @@
<span class="command" bindonce>
<span class="label" bo-class="getCommandKind(command.message)" bo-show="getCommandKind(command.message)"
bo-text="getCommandKind(command.message)">
</span>
<span class="command-title" bo-html="getCommandTitleHtml(command.message)"></span>
</span>

View file

@ -0,0 +1,4 @@
<span bindonce class="build-log-error-element">
<i class="fa fa-exclamation-triangle"></i>
<span class="error-message" bo-text="error.message"></span>
</span>

View file

@ -0,0 +1,4 @@
<span bindonce class="build-log-phase-element">
<span class="phase-icon" ng-class="phase.message"></span>
<span class="build-message" phase="phase.message"></span>
</span>

View file

@ -1 +1 @@
<span class="build-message-element">{{ getBuildMessage(build) }}</span>
<span class="build-message-element">{{ getBuildMessage(phase) }}</span>

View file

@ -1,5 +1,5 @@
<div class="build-progress-element">
<div class="progress" ng-class="getPercentage(build) < 100 ? 'active progress-striped' : ''" ng-show="getPercentage(build) > 0 && getPercentage(build) < 100">
<div class="progress" ng-class="getPercentage(build) < 100 ? 'active progress-striped' : ''">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ getPercentage(build) }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + getPercentage(build) + '%' }}">
</div>
</div>

View file

@ -1,7 +1,7 @@
<div id="build-status-container" class="build-status-container">
<div>
<span class="phase-icon" ng-class="build.phase"></span>
<span class="build-message" build="build"></span>
<span class="build-message" phase="build.phase"></span>
</div>
<div class="timing">
<i class="fa fa-clock-o"></i>

View file

@ -0,0 +1,27 @@
<div class="dockerfile-build-dialog-element">
<!-- Modal message dialog -->
<div class="modal fade" id="dockerfilebuildModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">
Start new Dockerfile build
</h4>
</div>
<div class="modal-body token-dialog-body">
<div class="alert alert-danger" ng-show="errorMessage">
{{ errorMessage }}
</div>
<div class="dockerfile-build-form" repository="repository" upload-failed="handleBuildFailed(message)"
build-started="handleBuildStarted(build)" build-failed="handleBuildFailed(message)" start-now="startCounter"
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="startBuild()" ng-disabled="building || uploading || !hasDockerfile">Start Build</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -0,0 +1,19 @@
<div class="dockerfile-build-form-element">
<div class="container" ng-show="building">
<div class="quay-spinner"></div>
</div>
<div class="container" ng-show="uploading">
<span class="message">Uploading file {{ upload_file }}</span>
<div class="progress progress-striped active">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ upload_progress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + upload_progress + '%' }}">
</div>
</div>
</div>
<div class="container" ng-show="!uploading && !building">
<div class="init-description">
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
</div>
<input id="file-drop" class="file-drop" type="file" file-present="internal.hasDockerfile">
</div>
</div>

View file

@ -15,7 +15,8 @@
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
<li><a href="http://docs.quay.io/">Documentation</a></li>
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}">Tutorial</a></li>
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}">Pricing</a></li>
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
</ul>

View file

@ -1,15 +1,25 @@
<span class="repo-breadcrumb-element">
<span ng-show="!image">
<span ng-show="!image && !subsection">
<span class="crumb">
<a href="{{ '/repository/?namespace=' + repo.namespace }}">{{repo.namespace}}</a>
</span>
<span class="current">{{repo.name}}</span>
</span>
<span ng-show="image">
<span ng-show="image && !subsection">
<span class="crumb">
<a href="{{ '/repository/?namespace=' + repo.namespace }}">{{repo.namespace}}</a>
</span>
<span class="crumb"><a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}">{{repo.name}}</a></span>
<span class="current">{{image.id.substr(0, 12)}}</span>
<span class="current">
<i class="fa fa-archive" style="margin-left: 2px; margin-right: 2px;"></i>
{{image.id.substr(0, 12)}}
</span>
</span>
<span ng-show="!image && subsection">
<span class="crumb">
<a href="{{ '/repository/?namespace=' + repo.namespace }}">{{repo.namespace}}</a>
</span>
<span class="crumb"><a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}">{{repo.name}}</a></span>
<span class="current"><i class="fa" ng-class="subsectionIcon" style="margin-left: 4px; margin-right: 8px;"></i>{{subsection}}</span>
</span>
</span>

View file

@ -102,8 +102,7 @@ function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
// Start the application code itself.
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce'], function($provide, cfpLoadingBarProvider) {
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml'], function($provide, cfpLoadingBarProvider) {
cfpLoadingBarProvider.includeSpinner = false;
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
@ -439,6 +438,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
var planService = {};
var listeners = [];
var previousSubscribeFailure = false;
planService.getFreePlan = function() {
return 'free';
};
@ -628,12 +629,15 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
if (orgname && !planService.isOrgCompatible(plan)) { return; }
planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && !cardInfo.last4) {
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
planService.showSubscribeDialog($scope, orgname, planId, callbacks);
return;
}
previousSubscribeFailure = false;
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
previousSubscribeFailure = true;
planService.handleCardError(resp);
callbacks['failure'](resp);
});
@ -793,13 +797,16 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
reloadOnSearch: false, controller: UserAdminCtrl}).
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html',
controller: GuideCtrl}).
when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using Quay.io', templateUrl: '/static/partials/tutorial.html',
controller: TutorialCtrl}).
when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html',
controller: ContactCtrl}).
when('/about/', {title: 'About Us', description:'Information about the Quay.io team and the company.', templateUrl: '/static/partials/about.html'}).
when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io',
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
templateUrl: '/static/partials/security.html', controller: SecurityCtrl}).
when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
templateUrl: '/static/partials/security.html'}).
when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html'}).
when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
@ -885,7 +892,9 @@ quayApp.directive('repoBreadcrumb', function () {
restrict: 'C',
scope: {
'repo': '=repo',
'image': '=image'
'image': '=image',
'subsection': '=subsection',
'subsectionIcon': '=subsectionIcon'
},
controller: function($scope, $element) {
}
@ -1134,6 +1143,13 @@ quayApp.directive('dockerAuthDialog', function () {
});
quayApp.filter('reverse', function() {
return function(items) {
return items.slice().reverse();
};
});
quayApp.filter('bytes', function() {
return function(bytes, precision) {
if (!bytes || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return 'Unknown';
@ -2476,6 +2492,119 @@ quayApp.directive('namespaceSelector', function () {
});
quayApp.directive('buildLogPhase', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-log-phase.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'phase': '=phase'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildLogError', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-log-error.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'error': '=error'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildLogCommand', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-log-command.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'command': '=command'
},
controller: function($scope, $element, $sanitize) {
var registryHandlers = {
'quay.io': function(pieces) {
var rnamespace = pieces[pieces.length - 2];
var rname = pieces[pieces.length - 1];
return '/repository/' + rnamespace + '/' + rname + '/';
},
'': function(pieces) {
var rnamespace = pieces.length == 1 ? '_' : pieces[0];
var rname = pieces[pieces.length - 1];
return 'https://index.docker.io/u/' + rnamespace + '/' + rname + '/';
}
};
var kindHandlers = {
'FROM': function(title) {
var pieces = title.split('/');
var registry = pieces.length < 3 ? '' : pieces[0];
if (!registryHandlers[registry]) {
return title;
}
return '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '" target="_blank">' + title + '</a>';
}
};
$scope.getCommandKind = function(fullTitle) {
var colon = fullTitle.indexOf(':');
var title = getTitleWithoutStep(fullTitle);
if (!title) {
return null;
}
var space = title.indexOf(' ');
return title.substring(0, space);
};
$scope.getCommandTitleHtml = function(fullTitle) {
var title = getTitleWithoutStep(fullTitle) || fullTitle;
var space = title.indexOf(' ');
if (space <= 0) {
return $sanitize(title);
}
var kind = $scope.getCommandKind(fullTitle);
var sanitized = $sanitize(title.substring(space + 1));
var handler = kindHandlers[kind || ''];
if (handler) {
return handler(sanitized);
} else {
return sanitized;
}
};
var getTitleWithoutStep = function(fullTitle) {
var colon = fullTitle.indexOf(':');
if (colon <= 0) {
return null;
}
return $.trim(fullTitle.substring(colon + 1));
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildStatus', function () {
var directiveDefinitionObject = {
priority: 0,
@ -2501,17 +2630,17 @@ quayApp.directive('buildMessage', function () {
transclude: false,
restrict: 'C',
scope: {
'build': '=build'
'phase': '=phase'
},
controller: function($scope, $element) {
$scope.getBuildMessage = function (buildInfo) {
switch (buildInfo.phase) {
$scope.getBuildMessage = function (phase) {
switch (phase) {
case 'starting':
case 'initializing':
return 'Starting Dockerfile build';
case 'waiting':
return 'Waiting for available build worker.';
return 'Waiting for available build worker';
case 'building':
return 'Building image from Dockerfile';
@ -2523,7 +2652,7 @@ quayApp.directive('buildMessage', function () {
return 'Dockerfile build completed and pushed';
case 'error':
return 'Dockerfile build failed.';
return 'Dockerfile build failed';
}
};
}
@ -2571,6 +2700,239 @@ quayApp.directive('buildProgress', function () {
return directiveDefinitionObject;
});
quayApp.directive('dockerfileBuildDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/dockerfile-build-dialog.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'showNow': '=showNow',
'buildStarted': '&buildStarted'
},
controller: function($scope, $element) {
$scope.building = false;
$scope.uploading = false;
$scope.startCounter = 0;
$scope.handleBuildStarted = function(build) {
$('#dockerfilebuildModal').modal('hide');
if ($scope.buildStarted) {
$scope.buildStarted({'build': build});
}
};
$scope.handleBuildFailed = function(message) {
$scope.errorMessage = message;
};
$scope.startBuild = function() {
$scope.errorMessage = null;
$scope.startCounter++;
};
$scope.$watch('showNow', function(sn) {
if (sn && $scope.repository) {
$('#dockerfilebuildModal').modal({});
}
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('dockerfileBuildForm', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/dockerfile-build-form.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'startNow': '=startNow',
'hasDockerfile': '=hasDockerfile',
'uploadFailed': '&uploadFailed',
'uploadStarted': '&uploadStarted',
'buildStarted': '&buildStarted',
'buildFailed': '&buildFailed',
'missingFile': '&missingFile',
'uploading': '=uploading',
'building': '=building'
},
controller: function($scope, $element, ApiService) {
$scope.internal = {'hasDockerfile': false};
var handleBuildFailed = function(message) {
message = message || 'Dockerfile build failed to start';
var result = false;
if ($scope.buildFailed) {
result = $scope.buildFailed({'message': message});
}
if (!result) {
bootbox.dialog({
"message": message,
"title": "Cannot start Dockerfile build",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
}
};
var handleUploadFailed = function(message) {
message = message || 'Error with file upload';
var result = false;
if ($scope.uploadFailed) {
result = $scope.uploadFailed({'message': message});
}
if (!result) {
bootbox.dialog({
"message": message,
"title": "Cannot upload file for Dockerfile build",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
}
};
var handleMissingFile = function() {
var result = false;
if ($scope.missingFile) {
result = $scope.missingFile({});
}
if (!result) {
bootbox.dialog({
"message": 'A Dockerfile or an archive containing a Dockerfile is required',
"title": "Missing Dockerfile",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
}
};
var startBuild = function(fileId) {
$scope.building = true;
var repo = $scope.repository;
var data = {
'file_id': fileId
};
var params = {
'repository': repo.namespace + '/' + repo.name
};
ApiService.requestRepoBuild(data, params).then(function(resp) {
$scope.building = false;
$scope.uploading = false;
if ($scope.buildStarted) {
$scope.buildStarted({'build': resp});
}
}, function(resp) {
$scope.building = false;
$scope.uploading = false;
handleBuildFailed(resp.message);
});
};
var conductUpload = function(file, url, fileId, mimeType) {
if ($scope.uploadStarted) {
$scope.uploadStarted({});
}
var request = new XMLHttpRequest();
request.open('PUT', url, true);
request.setRequestHeader('Content-Type', mimeType);
request.onprogress = function(e) {
$scope.$apply(function() {
var percentLoaded;
if (e.lengthComputable) {
$scope.upload_progress = (e.loaded / e.total) * 100;
}
});
};
request.onerror = function() {
$scope.$apply(function() {
handleUploadFailed();
});
};
request.onreadystatechange = function() {
var state = request.readyState;
if (state == 4) {
$scope.$apply(function() {
startBuild(fileId);
$scope.uploading = false;
});
return;
}
};
request.send(file);
};
var startFileUpload = function(repo) {
$scope.uploading = true;
$scope.uploading_progress = 0;
var uploader = $('#file-drop')[0];
if (uploader.files.length == 0) {
handleMissingFile();
$scope.uploading = false;
return;
}
var file = uploader.files[0];
$scope.upload_file = file.name;
var mimeType = file.type || 'application/octet-stream';
var data = {
'mimeType': mimeType
};
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
conductUpload(file, resp.url, resp.file_id, mimeType);
}, function() {
handleUploadFailed('Could not retrieve upload URL');
});
};
$scope.$watch('internal.hasDockerfile', function(d) {
$scope.hasDockerfile = d;
});
$scope.$watch('startNow', function() {
if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
startFileUpload();
}
});
}
};
return directiveDefinitionObject;
});
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
quayApp.directive('ngBlur', function() {
return function( scope, elem, attrs ) {
@ -2580,6 +2942,22 @@ quayApp.directive('ngBlur', function() {
};
});
quayApp.directive("filePresent", [function () {
return {
restrict: 'A',
scope: {
'filePresent': "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
scope.$apply(function() {
scope.filePresent = changeEvent.target.files.length > 0;
});
});
}
}
}]);
quayApp.directive('ngVisible', function () {
return function (scope, element, attr) {
scope.$watch(attr.ngVisible, function (visible) {

View file

@ -15,8 +15,14 @@ $.fn.clipboardCopy = function() {
});
};
function SigninCtrl($scope) {
};
function GuideCtrl() {
}
function SecurityCtrl($scope) {
}
function ContactCtrl($scope) {
}
function PlansCtrl($scope, $location, UserService, PlanService) {
// Load the list of plans.
@ -42,13 +48,153 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
};
}
function GuideCtrl($scope) {
}
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
// Default to showing sudo on all commands if on linux.
var showSudo = navigator.appVersion.indexOf("Linux") != -1;
function SecurityCtrl($scope) {
}
function ContactCtrl($scope) {
$scope.tour = {
'title': 'Quay.io Tutorial',
'initialScope': {
'showSudo': showSudo
},
'steps': [
{
'title': 'Welcome to the Quay.io tutorial!',
'templateUrl': '/static/tutorial/welcome.html'
},
{
'title': 'Sign in to get started',
'templateUrl': '/static/tutorial/signup.html',
'signal': function($tourScope) {
var user = UserService.currentUser();
$tourScope.username = user.username;
$tourScope.email = user.email;
$tourScope.inOrganization = user.organizations && user.organizations.length > 0;
return !user.anonymous;
}
},
{
'title': 'Step 1: Login to Quay.io',
'templateUrl': '/static/tutorial/docker-login.html',
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message) {
return message['data']['action'] == 'login';
}),
'waitMessage': "Waiting for docker login",
'skipTitle': "I'm already logged in",
'mixpanelEvent': 'tutorial_start'
},
{
'title': 'Step 2: Create a new container',
'templateUrl': '/static/tutorial/create-container.html'
},
{
'title': 'Step 3: Create a new image',
'templateUrl': '/static/tutorial/create-image.html'
},
{
'title': 'Step 4: Push the image to Quay.io',
'templateUrl': '/static/tutorial/push-image.html',
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message, tourScope) {
var pushing = message['data']['action'] == 'push_repo';
if (pushing) {
tourScope.repoName = message['data']['repository'];
}
return pushing;
}),
'waitMessage': "Waiting for repository push to begin",
'mixpanelEvent': 'tutorial_wait_for_push'
},
{
'title': 'Push in progress',
'templateUrl': '/static/tutorial/pushing.html',
'signal': AngularTourSignals.serverEvent('/realtime/user/subscribe?events=docker-cli',
function(message, tourScope) {
return message['data']['action'] == 'pushed_repo';
}),
'waitMessage': "Waiting for repository push to complete"
},
{
'title': 'Step 5: View the repository on Quay.io',
'templateUrl': '/static/tutorial/view-repo.html',
'signal': AngularTourSignals.matchesLocation('/repository/'),
'overlayable': true,
'mixpanelEvent': 'tutorial_push_complete'
},
{
'templateUrl': '/static/tutorial/view-repo.html',
'signal': AngularTourSignals.matchesLocation('/repository/'),
'overlayable': true
},
{
'templateUrl': '/static/tutorial/waiting-repo-list.html',
'signal': AngularTourSignals.elementAvaliable('*[data-repo="{{username}}/{{repoName}}"]'),
'overlayable': true
},
{
'templateUrl': '/static/tutorial/repo-list.html',
'signal': AngularTourSignals.matchesLocation('/repository/{{username}}/{{repoName}}'),
'element': '*[data-repo="{{username}}/{{repoName}}"]',
'overlayable': true
},
{
'title': 'Repository View',
'content': 'This is the repository view page. It displays all the primary information about your repository.',
'overlayable': true,
'mixpanelEvent': 'tutorial_view_repo'
},
{
'title': 'Image History',
'content': 'The tree displays the full history of your repository, including all its tag. ' +
'You can click on a tag or image to see its information.',
'element': '#image-history-container',
'overlayable': true
},
{
'title': 'Tag/Image Information',
'content': 'This panel displays information about the currently selected tag or image',
'element': '#side-panel',
'overlayable': true
},
{
'title': 'Select tag or image',
'content': 'You can select a tag or image by clicking on this dropdown',
'element': '#side-panel-dropdown',
'overlayable': true
},
{
'content': 'To view the admin settings for the repository, click on the gear',
'element': '#admin-cog',
'signal': AngularTourSignals.matchesLocation('/repository/{{username}}/{{repoName}}/admin'),
'overlayable': true
},
{
'title': 'Repository Admin',
'content': "The repository admin panel allows for modification of a repository's permissions, webhooks, visibility and other settings",
'overlayable': true,
'mixpanelEvent': 'tutorial_view_admin'
},
{
'title': 'Permissions',
'templateUrl': '/static/tutorial/permissions.html',
'overlayable': true,
'element': '#permissions'
},
{
'title': 'Adding a permission',
'content': 'To add an <b>additional</b> permission, enter a username or robot account name into the autocomplete ' +
'or hit the dropdown arrow to manage robot accounts',
'overlayable': true,
'element': '#add-entity-permission'
},
{
'templateUrl': '/static/tutorial/done.html',
'overlayable': true,
'mixpanelEvent': 'tutorial_complete'
}
]
};
}
function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
@ -195,8 +341,17 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
// Start scope methods //////////////////////////////////////////
$scope.buildDialogShowCounter = 0;
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
$scope.showNewBuildDialog = function() {
$scope.buildDialogShowCounter++;
};
$scope.handleBuildStarted = function(build) {
startBuildInfoTimer($scope.repo);
};
$scope.showBuild = function(buildInfo) {
$location.path('/repository/' + namespace + '/' + name + '/build');
$location.search('current', buildInfo.id);
@ -619,37 +774,11 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
loadViewInfo();
}
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize) {
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var pollTimerHandle = null;
var registryHandlers = {
'quay.io': function(pieces) {
var rnamespace = pieces[pieces.length - 2];
var rname = pieces[pieces.length - 1];
return '/repository/' + rnamespace + '/' + rname + '/';
},
'': function(pieces) {
var rnamespace = pieces.length == 1 ? '_' : pieces[0];
var rname = pieces[pieces.length - 1];
return 'https://index.docker.io/' + rnamespace + '/' + rname + '/';
}
};
var kindHandlers = {
'FROM': function(title) {
var pieces = title.split('/');
var registry = pieces.length < 2 ? '' : pieces[0];
if (!registryHandlers[registry]) {
return title;
}
return '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '">' + title + '</a>';
}
};
$scope.$on('$destroy', function() {
stopPollTimer();
});
@ -664,50 +793,53 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.builds = [];
$scope.polling = false;
$scope.buildDialogShowCounter = 0;
$scope.showNewBuildDialog = function() {
$scope.buildDialogShowCounter++;
};
$scope.handleBuildStarted = function(newBuild) {
$scope.builds.push(newBuild);
$scope.setCurrentBuild(newBuild['id'], true);
};
$scope.adjustLogHeight = function() {
$('.build-logs').height($(window).height() - 365);
$('.build-logs').height($(window).height() - 385);
};
$scope.getShortId = function(id) {
var lastIndex = id.lastIndexOf('-');
return id.substr(lastIndex + 1);
$scope.askRestartBuild = function(build) {
$('#confirmRestartBuildModal').modal({});
};
$scope.getCommandKind = function(fullTitle) {
var colon = fullTitle.indexOf(':');
var title = getTitleWithoutStep(fullTitle);
if (!title) {
return null;
}
$scope.restartBuild = function(build) {
$('#confirmRestartBuildModal').modal('hide');
var space = title.indexOf(' ');
return title.substring(0, space);
var data = {
'file_id': build['resource_key']
};
var params = {
'repository': namespace + '/' + name
};
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
$scope.builds.push(newBuild);
$scope.setCurrentBuild(newBuild['id'], true);
});
};
$scope.getCommandTitleHtml = function(fullTitle) {
var title = getTitleWithoutStep(fullTitle) || fullTitle;
var space = title.indexOf(' ');
if (space <= 0) {
return $sanitize(title);
}
$scope.hasLogs = function(container) {
return ((container.logs && container.logs.length) || (container._logs && container._logs.length));
};
var kind = $scope.getCommandKind(fullTitle);
var sanitized = $sanitize(title.substring(space + 1));
var handler = kindHandlers[kind || ''];
if (handler) {
return handler(sanitized);
$scope.toggleLogs = function(container) {
if (container._logs) {
container.logs = container._logs;
container._logs = null;
} else {
return sanitized;
}
};
$scope.toggleCommand = function(command) {
command.expanded = !command.expanded;
if (command.expanded && !command.logs) {
// Load the logs for the command.
loadCommandLogs(command);
container._logs = container.logs;
container.logs = null;
}
};
@ -715,22 +847,33 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
// Find the build.
for (var i = 0; i < $scope.builds.length; ++i) {
if ($scope.builds[i].id == buildId) {
$scope.setCurrentBuildInternal($scope.builds[i], opt_updateURL);
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
return;
}
}
};
$scope.setCurrentBuildInternal = function(build, opt_updateURL) {
$scope.processANSI = function(message, container) {
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
// Note: order is important here.
var setup = filter.getSetupHtml();
var stream = filter.addInputToStream(message);
var teardown = filter.getTeardownHtml();
return setup + stream + teardown;
};
$scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
if (build == $scope.currentBuild) { return; }
stopPollTimer();
$scope.commands = null;
$scope.commandMap = {};
$scope.logs = null;
$scope.logStartIndex = 0;
$scope.logEntries = null;
$scope.logStartIndex = null;
$scope.currentParentEntry = null;
$scope.currentBuild = build;
$scope.currentBuildIndex = index;
if (opt_updateURL) {
if (build) {
@ -746,19 +889,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.adjustLogHeight();
}, 1);
// Load the first set of logs.
getBuildStatusAndLogs();
// If the build is currently processing, start the build timer.
checkPollTimer();
};
var getTitleWithoutStep = function(fullTitle) {
var colon = fullTitle.indexOf(':');
if (colon <= 0) {
return null;
}
return $.trim(fullTitle.substring(colon + 1));
}
var checkPollTimer = function() {
var build = $scope.currentBuild;
if (!build) {
@ -785,68 +922,29 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
};
var processLogs = function(logs, startIndex) {
var currentCommand = $scope.commands.length > 0 ? $scope.commands[$scope.commands.length - 1] : null;
if (!$scope.logEntries) { $scope.logEntries = []; }
for (var i = 0; i < logs.length; ++i) {
var entry = logs[i];
if (entry['is_command']) {
var existing = $scope.commandMap[entry['message']];
if (existing) {
currentCommand = existing;
continue;
var type = entry['type'] || 'entry';
if (type == 'command' || type == 'phase' || type == 'error') {
entry['_logs'] = [];
entry['index'] = startIndex + i;
$scope.logEntries.push(entry);
$scope.currentParentEntry = entry;
} else if ($scope.currentParentEntry) {
if ($scope.currentParentEntry['logs']) {
$scope.currentParentEntry['logs'].push(entry);
} else {
$scope.currentParentEntry['_logs'].push(entry);
}
currentCommand = {
'message': entry['message'],
'index': startIndex + i
};
$scope.commands.push(currentCommand);
$scope.commandMap[entry['message']] = currentCommand;
continue;
}
if (!currentCommand.logs) {
currentCommand.logs = [];
}
currentCommand.logs.push(entry);
}
if (currentCommand.expanded == null) {
currentCommand.expanded = true;
}
};
var loadCommandLogs = function(command) {
var start = command['index'] + 1;
var end = null;
var currentCommandIndex = jQuery.inArray(command, $scope.commands);
if (currentCommandIndex >= 0 && currentCommandIndex < $scope.commands.length - 1) {
var nextCommand = $scope.commands[currentCommandIndex + 1];
end = nextCommand.index ? nextCommand.index - 1 : null;
}
var params = {
'repository': namespace + '/' + name,
'build_uuid': $scope.currentBuild.id
};
var options = {
'start': start
};
if (end != null) {
options['end'] = end;
}
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
if (resp['logs']) {
command.logs = resp['logs'];
}
});
};
var getBuildStatusAndLogs = function() {
if (!$scope.currentBuild || $scope.polling) { return; }
$scope.polling = true;
var params = {
@ -857,22 +955,25 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
// Note: We use extend here rather than replacing as Angular is depending on the
// root build object to remain the same object.
$.extend(true, $scope.currentBuild, resp);
$.extend(true, $scope.builds[$scope.currentBuildIndex], resp);
checkPollTimer();
// Load the updated logs for the build.
var options = {
'commands': $scope.commands == null,
'start': $scope.logStartIndex
};
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
if (resp['commands']) {
$scope.commands = resp['commands'];
if ($scope.logStartIndex != null && resp['start'] != $scope.logStartIndex) {
$scope.polling = false;
return;
}
processLogs(resp.logs, $scope.logStartIndex);
processLogs(resp['logs'], resp['start']);
$scope.logStartIndex = resp['total'];
$scope.polling = false;
}, function() {
$scope.polling = false;
});
});
};
@ -881,8 +982,16 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
var params = {'repository': namespace + '/' + name};
$rootScope.title = 'Loading Repository...';
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
if (!repo.can_write) {
$rootScope.title = 'Unknown builds';
$scope.accessDenied = true;
return;
}
$rootScope.title = 'Repository Builds';
$scope.repo = repo;
getBuildInfo();
});
};
@ -897,13 +1006,12 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
if ($location.search().current) {
$scope.setCurrentBuild($location.search().current, false);
} else if ($scope.builds.length > 0) {
$scope.setCurrentBuild($scope.builds[0].id, true);
$scope.setCurrentBuild($scope.builds[$scope.builds.length - 1].id, true);
}
});
};
fetchRepository();
getBuildInfo();
}
function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope) {
@ -914,8 +1022,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.logsShown = 0;
$scope.deleting = false;
$scope.permissionCache = {};
$scope.buildEntityForPermission = function(name, permission, kind) {
return {
var key = name + ':' + kind;
if ($scope.permissionCache[key]) {
return $scope.permissionCache[key];
}
return $scope.permissionCache[key] = {
'kind': kind,
'name': name,
'is_robot': permission.is_robot,
@ -936,7 +1051,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.addNewPermission = function(entity) {
// Don't allow duplicates.
if ($scope.permissions[entity.kind][entity.name]) { return; }
if (!entity || !entity.kind || $scope.permissions[entity.kind][entity.name]) { return; }
if (entity.is_org_member === false) {
$scope.currentAddEntity = entity;
@ -1156,8 +1271,13 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
};
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
$scope.repo = repo;
if (!repo.can_admin) {
$rootScope.title = 'Forbidden';
$scope.accessDenied = true;
return;
}
$scope.repo = repo;
$rootScope.title = 'Settings - ' + namespace + '/' + name;
$rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
': Permissions, webhooks and other settings';
@ -1448,12 +1568,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
'initialize': false
};
$('#couldnotbuildModal').on('hidden.bs.modal', function() {
$scope.$apply(function() {
$location.path('/repository/' + $scope.created.namespace + '/' + $scope.created.name);
});
});
// Watch the namespace on the repo. If it changes, we update the plan and the public/private
// accordingly.
$scope.isUserNamespace = true;
@ -1500,15 +1614,36 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
$scope.repo.namespace = namespace;
};
$scope.handleBuildStarted = function() {
var repo = $scope.repo;
$location.path('/repository/' + repo.namespace + '/' + repo.name);
};
$scope.handleBuildFailed = function(message) {
var repo = $scope.repo;
bootbox.dialog({
"message": message,
"title": "Could not start Dockerfile build",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary",
"callback": function() {
$scope.$apply(function() {
$location.path('/repository/' + repo.namespace + '/' + repo.name);
});
}
}
}
});
return true;
};
$scope.createNewRepo = function() {
$('#repoName').popover('hide');
var uploader = $('#file-drop')[0];
if ($scope.repo.initialize && uploader.files.length < 1) {
$('#missingfileModal').modal();
return;
}
$scope.creating = true;
var repo = $scope.repo;
var data = {
@ -1524,7 +1659,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
// Repository created. Start the upload process if applicable.
if ($scope.repo.initialize) {
startFileUpload(created);
$scope.createdForBuild = created;
return;
}
@ -1554,74 +1689,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
};
var startBuild = function(repo, fileId) {
$scope.building = true;
var data = {
'file_id': fileId
};
var params = {
'repository': repo.namespace + '/' + repo.name
};
ApiService.requestRepoBuild(data, params).then(function(resp) {
$location.path('/repository/' + params.repository);
}, function() {
$('#couldnotbuildModal').modal();
});
};
var conductUpload = function(repo, file, url, fileId, mimeType) {
var request = new XMLHttpRequest();
request.open('PUT', url, true);
request.setRequestHeader('Content-Type', mimeType);
request.onprogress = function(e) {
$scope.$apply(function() {
var percentLoaded;
if (e.lengthComputable) {
$scope.upload_progress = (e.loaded / e.total) * 100;
}
});
};
request.onerror = function() {
$scope.$apply(function() {
$('#couldnotbuildModal').modal();
});
};
request.onreadystatechange = function() {
var state = request.readyState;
if (state == 4) {
$scope.$apply(function() {
$scope.uploading = false;
startBuild(repo, fileId);
});
return;
}
};
request.send(file);
};
var startFileUpload = function(repo) {
$scope.uploading = true;
$scope.uploading_progress = 0;
var uploader = $('#file-drop')[0];
var file = uploader.files[0];
$scope.upload_file = file.name;
var mimeType = file.type || 'application/octet-stream';
var data = {
'mimeType': mimeType
};
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
conductUpload(repo, file, resp.url, resp.file_id, mimeType);
}, function() {
$('#couldnotbuildModal').modal();
});
};
var subscribedToPlan = function(sub) {
$scope.planChanging = false;
$scope.subscription = sub;
@ -1839,7 +1906,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
$rootScope.title = 'Loading...';
$scope.addNewMember = function(member) {
if ($scope.members[member.name]) { return; }
if (!member || $scope.members[member.name]) { return; }
var params = {
'orgname': orgname,

284
static/js/tour.js Normal file
View file

@ -0,0 +1,284 @@
angular.module("angular-tour", [])
.provider('AngularTour', function() {
this.$get = ['$document', '$rootScope', '$compile', '$location', function($document, $rootScope, $compile, $location) {
$rootScope.angular_tour_current = null;
function _start(tour, opt_stepIndex, opt_existingScope) {
tour.initialStep = opt_stepIndex || tour.initialStep || 0;
tour.tourScope = opt_existingScope || null;
$rootScope.angular_tour_current = tour;
}
function _stop() {
$rootScope.angular_tour_current = null;
}
return {
start: _start,
stop: _stop
};
}];
})
.directive('angularTourUi', function() {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/angular-tour-ui.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'tour': '=tour',
'inline': '=inline',
},
controller: function($rootScope, $scope, $element, $location, $interval, AngularTour) {
var createNewScope = function(initialScope) {
var tourScope = jQuery.extend({}, initialScope || {});
tourScope['_replaceData'] = function(s) {
if (typeof s != 'string') {
return s;
}
for (var key in tourScope) {
if (key[0] == '_') { continue; }
if (tourScope.hasOwnProperty(key)) {
s = s.replace('{{' + key + '}}', tourScope[key]);
}
}
return s;
};
return tourScope;
};
$scope.stepIndex = 0;
$scope.step = null;
$scope.interval = null;
$scope.tourScope = null;
var getElement = function() {
if (typeof $scope.step['element'] == 'function') {
return $($scope.step['element'](tourScope));
}
return $($scope.tourScope._replaceData($scope.step['element']));
};
var checkSignal = function() {
return $scope.step['signal'] && $scope.step['signal']($scope.tourScope);
};
var teardownSignal = function() {
if (!$scope.step) { return; }
var signal = $scope.step['signal'];
if (signal && signal.$teardown) {
signal.$teardown($scope.tourScope);
}
};
var setupSignal = function() {
if (!$scope.step) { return; }
var signal = $scope.step['signal'];
if (signal && signal.$setup) {
signal.$setup($scope.tourScope);
}
};
var checkSignalTimer = function() {
if (!$scope.step || !$scope.tourScope) {
stopSignalTimer();
return;
}
if (checkSignal()) {
$scope.next();
}
};
var stopSignalTimer = function() {
if (!$scope.interval) { return; }
$interval.cancel($scope.interval);
$scope.interval = null;
};
var startSignalTimer = function() {
$scope.interval = $interval(checkSignalTimer, 500);
};
var closeDomHighlight = function() {
if (!$scope.step) { return; }
var element = getElement($scope.tourScope);
element.spotlight('close');
};
var updateDomHighlight = function() {
var element = getElement();
if (!element.length) {
return;
}
element.spotlight({
opacity: .5,
speed: 400,
color: '#333',
animate: true,
easing: 'linear',
exitEvent: 'mouseenter',
exitEventAppliesToElement: true,
paddingX: 1,
paddingY: 1
});
};
var fireMixpanelEvent = function() {
if (!$scope.step || !mixpanel) { return; }
var eventName = $scope.step['mixpanelEvent'];
if (eventName) {
mixpanel.track(eventName);
}
};
$scope.setStepIndex = function(stepIndex) {
// Close existing spotlight and signal timer.
teardownSignal();
closeDomHighlight();
stopSignalTimer();
// Check if there is a next step.
if (!$scope.tour || stepIndex >= $scope.tour.steps.length) {
$scope.step = null;
$scope.hasNextStep = false;
return;
}
$scope.step = $scope.tour.steps[stepIndex];
fireMixpanelEvent();
// If the signal is already true, then skip this step entirely.
setupSignal();
if (checkSignal()) {
$scope.setStepIndex(stepIndex + 1);
return;
}
$scope.stepIndex = stepIndex;
$scope.hasNextStep = stepIndex < $scope.tour.steps.length - 1;
// Need the timeout here to ensure the click event does not
// hide the spotlight, and it has to be longer than the hide
// spotlight animation timing.
setTimeout(function() {
updateDomHighlight();
}, 500);
// Start listening for signals to move the tour forward.
if ($scope.step.signal) {
startSignalTimer();
}
};
$scope.stop = function() {
closeDomHighlight();
$scope.tour = null;
AngularTour.stop();
};
$scope.next = function() {
$scope.setStepIndex($scope.stepIndex + 1);
};
$scope.$watch('tour', function(tour) {
stopSignalTimer();
if (tour) {
// Set the tour scope.
if (tour.tourScope) {
$scope.tourScope = tour.tourScope;
} else {
$scope.tourScope = $scope.tour.tourScope = createNewScope(tour.initialScope);
}
// Set the initial step.
$scope.setStepIndex(tour.initialStep || 0);
}
});
// If this is an inline tour, then we need to monitor the page to determine when
// to transition it to an overlay tour.
if ($scope.inline) {
var counter = 0;
var unbind = $rootScope.$watch(function() {
return $location.path();
}, function(location) {
// Since this callback fires for the first page display, we only unbind it
// after the second call.
if (counter == 1) {
// Unbind the listener.
unbind();
// Teardown any existing signal listener.
teardownSignal();
// If there is an active tour, transition it over to the overlay.
if ($scope.tour && $scope.step && $scope.step['overlayable']) {
AngularTour.start($scope.tour, $scope.stepIndex + 1, $scope.tourScope);
$scope.tour = null;
}
}
counter++;
});
}
}
};
return directiveDefinitionObject;
})
.factory('AngularTourSignals', ['$location', function($location) {
var signals = {};
// Signal: When the page location matches the given path.
signals.matchesLocation = function(locationPath) {
return function(tourScope) {
return $location.path() == tourScope._replaceData(locationPath);
};
};
// Signal: When an element is found in the page's DOM.
signals.elementAvaliable = function(elementPath) {
return function(tourScope) {
return $(tourScope._replaceData(elementPath)).length > 0;
};
};
// Signal: When a server-side event matches the predicate.
signals.serverEvent = function(url, matcher) {
var checker = function(tourScope) {
return checker.$message && matcher(checker.$message, tourScope);
};
checker.$message = null;
checker.$setup = function(tourScope) {
var fullUrl = tourScope._replaceData(url);
checker.$source = new EventSource(fullUrl);
checker.$source.onmessage = function(e) {
checker.$message = JSON.parse(e.data);
};
};
checker.$teardown = function() {
if (checker.$source) {
checker.$source.close();
}
};
return checker;
};
return signals;
}]);

335
static/lib/ansi2html.js Normal file
View file

@ -0,0 +1,335 @@
/**
* Originally from: https://github.com/jorgeecardona/ansi-to-html
* Modified by jschorr: Add ability to repeat existing styles and not close them out automatically.
*/
angular.module('ansiToHtml', []).value('ansi2html', (function() {
// Define the styles supported from ANSI.
var STYLES = {
'ef0': 'color:#000',
'ef1': 'color:#A00',
'ef2': 'color:#0A0',
'ef3': 'color:#A50',
'ef4': 'color:#00A',
'ef5': 'color:#A0A',
'ef6': 'color:#0AA',
'ef7': 'color:#AAA',
'ef8': 'color:#555',
'ef9': 'color:#F55',
'ef10': 'color:#5F5',
'ef11': 'color:#FF5',
'ef12': 'color:#55F',
'ef13': 'color:#F5F',
'ef14': 'color:#5FF',
'ef15': 'color:#FFF',
'eb0': 'background-color:#000',
'eb1': 'background-color:#A00',
'eb2': 'background-color:#0A0',
'eb3': 'background-color:#A50',
'eb4': 'background-color:#00A',
'eb5': 'background-color:#A0A',
'eb6': 'background-color:#0AA',
'eb7': 'background-color:#AAA',
'eb8': 'background-color:#555',
'eb9': 'background-color:#F55',
'eb10': 'background-color:#5F5',
'eb11': 'background-color:#FF5',
'eb12': 'background-color:#55F',
'eb13': 'background-color:#F5F',
'eb14': 'background-color:#5FF',
'eb15': 'background-color:#FFF'
};
// Define the default styles.
var DEFAULTS = {
fg: '#FFF',
bg: '#000'
};
var __slice = [].slice;
var toHexString = function(num) {
num = num.toString(16);
while (num.length < 2) {
num = "0" + num;
}
return num;
};
var escapeHtml = function (unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
// Define the derived styles.
[0, 1, 2, 3, 4, 5].forEach(
function(red) {
return [0, 1, 2, 3, 4, 5].forEach(
function(green) {
return [0, 1, 2, 3, 4, 5].forEach(
function(blue) {
var b, c, g, n, r, rgb;
c = 16 + (red * 36) + (green * 6) + blue;
r = red > 0 ? red * 40 + 55 : 0;
g = green > 0 ? green * 40 + 55 : 0;
b = blue > 0 ? blue * 40 + 55 : 0;
rgb = ((function() {
var _i, _len, _ref, _results;
_ref = [r, g, b];
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
n = _ref[_i];
_results.push(toHexString(n));
}
return _results;
})()).join('');
STYLES["ef" + c] = "color:#" + rgb;
return STYLES["eb" + c] = "background-color:#" + rgb;
});
});
});
(function() {
_results = [];
for (_i = 0; _i <= 23; _i++){ _results.push(_i); }
return _results;
}).apply(this).forEach(
function(gray) {
var c, l;
c = gray + 232;
l = toHexString(gray * 10 + 8);
STYLES["ef" + c] = "color:#" + l + l + l;
return STYLES["eb" + c] = "background-color:#" + l + l + l;
});
// Define the filter class which will track all the ANSI style state.
function Filter(options) {
this.opts = $.extend(true, DEFAULTS, options || {});
this.input = [];
this.stack = [];
}
Filter.create = function() {
return new Filter();
};
Filter.prototype.toHtml = function(input) {
this.resetStyles();
var html = this.addInputToStream(input) + this.getTeardownHtml();
this.resetStyles();
return html;
};
Filter.prototype.addInputToStream = function(input) {
var buf = [];
this.input = typeof input === 'string' ? [input] : input;
this.forEach(function(chunk) {
return buf.push(chunk);
});
return buf.join('');
};
Filter.prototype.getSetupHtml = function() {
return this.stack.map(function(data) {
return data['html'];
}).join('');
};
Filter.prototype.getTeardownHtml = function() {
var stackCopy = this.stack.slice();
return stackCopy.reverse().map(function(data) {
return "</" + data['kind'] + ">";
}).join('');
};
Filter.prototype.forEach = function(callback) {
var that = this;
var buf = '';
var handleDisplay = function(code) {
code = parseInt(code, 10);
if (code === -1) {
callback('<br/>');
}
if (code === 0) {
callback(that.getTeardownHtml());
that.resetStyles();
}
if (code === 1) {
callback(that.pushTag('b'));
}
if (code === 2) {
}
if ((2 < code && code < 5)) {
callback(that.pushTag('u'));
}
if ((4 < code && code < 7)) {
callback(that.pushTag('blink'));
}
if (code === 7) {
}
if (code === 8) {
callback(that.pushStyle('display:none'));
}
if (code === 9) {
callback(that.pushTag('strike'));
}
if (code === 24) {
callback(that.closeTag('u'));
}
if ((29 < code && code < 38)) {
callback(that.pushStyle("ef" + (code - 30)));
}
if (code === 39) {
callback(that.pushStyle("color:" + that.opts.fg));
}
if ((39 < code && code < 48)) {
callback(that.pushStyle("eb" + (code - 40)));
}
if (code === 49) {
callback(that.pushStyle("background-color:" + that.opts.bg));
}
if ((89 < code && code < 98)) {
callback(that.pushStyle("ef" + (8 + (code - 90))));
}
if ((99 < code && code < 108)) {
return callback(that.pushStyle("eb" + (8 + (code - 100))));
}
};
this.input.forEach(function(chunk) {
buf += chunk;
return that.tokenize(buf, function(tok, data) {
switch (tok) {
case 'text':
return callback(escapeHtml(data));
case 'display':
return handleDisplay(data);
case 'xterm256':
return callback(that.pushStyle("ef" + data));
}
});
});
};
Filter.prototype.pushTag = function(tag, style) {
if (style == null) {
style = '';
}
if (style.length && style.indexOf(':') === -1) {
style = STYLES[style];
}
var html = ["<" + tag, (style ? " style=\"" + style + "\"" : void 0), ">"].join('');
this.stack.push({'html': html, 'kind': tag});
return html;
};
Filter.prototype.pushStyle = function(style) {
return this.pushTag("span", style);
};
Filter.prototype.closeTag = function(style) {
var last;
if (this.stack.slice(-1)[0]['kind'] === style) {
last = this.stack.pop();
}
if (last != null) {
return "</" + style + ">";
}
};
Filter.prototype.resetStyles = function() {
this.stack = [];
};
Filter.prototype.tokenize = function(text, callback) {
var ansiHandler, ansiMatch, ansiMess, handler, i, length, newline, process, realText, remove, removeXterm256, tokens, _j, _len, _results1,
_this = this;
ansiMatch = false;
ansiHandler = 3;
remove = function(m) {
return '';
};
removeXterm256 = function(m, g1) {
callback('xterm256', g1);
return '';
};
newline = function(m) {
if (_this.opts.newline) {
callback('display', -1);
} else {
callback('text', m);
}
return '';
};
ansiMess = function(m, g1) {
var code, _j, _len;
ansiMatch = true;
if (g1.trim().length === 0) {
g1 = '0';
}
g1 = g1.trimRight(';').split(';');
for (_j = 0, _len = g1.length; _j < _len; _j++) {
code = g1[_j];
callback('display', code);
}
return '';
};
realText = function(m) {
callback('text', m);
return '';
};
tokens = [
{
pattern: /^\x08+/,
sub: remove
}, {
pattern: /^\x1b\[38;5;(\d+)m/,
sub: removeXterm256
}, {
pattern: /^\n+/,
sub: newline
}, {
pattern: /^\x1b\[((?:\d{1,3};?)+|)m/,
sub: ansiMess
}, {
pattern: /^\x1b\[?[\d;]{0,3}/,
sub: remove
}, {
pattern: /^([^\x1b\x08\n]+)/,
sub: realText
}
];
process = function(handler, i) {
var matches;
if (i > ansiHandler && ansiMatch) {
return;
} else {
ansiMatch = false;
}
matches = text.match(handler.pattern);
text = text.replace(handler.pattern, handler.sub);
};
_results1 = [];
while ((length = text.length) > 0) {
for (i = _j = 0, _len = tokens.length; _j < _len; i = ++_j) {
handler = tokens[i];
process(handler, i);
}
if (text.length === length) {
break;
} else {
_results1.push(void 0);
}
}
return _results1;
};
return Filter;
})());

View file

@ -0,0 +1,197 @@
/**
* jQuery Spotlight
*
* Project Page: http://github.com/
* Original Plugin code by Gilbert Pellegrom (2009)
* Licensed under the GPL license (http://www.gnu.org/licenses/gpl-3.0.html)
* Version 1.1 (2011)
* Modified by jschorr (Fix Opacity bug, fix handling of events, add rounded corners)
*/
(function ($) {
var currentOverlay;
$.fn.spotlight = function (options) {
var method = 'create';
// Default settings
settings = $.extend({}, {
opacity: .5,
speed: 400,
color: '#333',
animate: true,
easing: '',
exitEvent: 'click',
exitEventAppliesToElement: false,
onShow: function () {
// do nothing
},
onHide: function () {
// do nothing
},
spotlightZIndex: 9999,
spotlightElementClass: 'spotlight-background',
parentSelector: 'html',
paddingX: 0,
paddingY: 0
}, options);
function closeOverlay () {
if (!currentOverlay) {
return;
}
if (settings.animate) {
currentOverlay.animate({opacity: 0}, settings.speed, settings.easing, function () {
if (currentOverlay != null) {
currentOverlay.remove();
currentOverlay = null;
// Trigger the onHide callback
settings.onHide.call(this);
}
});
} else {
currentOverlay.remove();
currentOverlay = null;
// Trigger the onHide callback
settings.onHide.call(this);
}
}
if (typeof options === 'string') {
method = options;
options = arguments[1];
}
switch (method) {
case 'close':
case 'destroy':
closeOverlay();
return;
}
var elements = $(this),
overlay,
parent,
context;
function roundRect(context, x, y, w, h, r) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
context.beginPath();
context.moveTo(x+r, y);
context.arcTo(x+w, y, x+w, y+h, r);
context.arcTo(x+w, y+h, x, y+h, r);
context.arcTo(x, y+h, x, y, r);
context.arcTo(x, y, x+w, y, r);
context.closePath();
return context;
}
/**
* Colour in the overlay and clear all element masks
*/
function fillOverlay () {
context.fillStyle = settings.color;
context.fillRect(0, 0, parent.innerWidth(), parent.innerHeight());
// loop through elements and clear their position
elements.each(function (i, e) {
var ej = $(e);
var currentPos = e.getBoundingClientRect();
context.save();
context.globalCompositeOperation = 'destination-out';
roundRect(context, currentPos.left - settings.paddingX,
currentPos.top - settings.paddingY,
ej.outerWidth() + (settings.paddingX * 2),
ej.outerHeight() + (settings.paddingY * 2),
6).fill();
context.restore();
});
}
/**
* Handle resizing the window
*
* @param e
*/
function handleResize (e) {
overlay.attr('width', parent.innerWidth());
overlay.attr('height', parent.innerHeight());
if (typeof context !== 'undefined') {
fillOverlay();
}
}
closeOverlay();
// Add the overlay element
overlay = $('<canvas></canvas>');
overlay.addClass(settings.spotlightElementClass);
currentOverlay = overlay;
parent = $(settings.parentSelector);
parent.append(overlay);
// Get our elements
var element = $(this);
// Set the CSS styles
var cssConfig = {
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
zIndex: settings.spotlightZIndex,
opacity: 0
};
if (settings.parentSelector == 'html') {
parent.css('height', '100%');
}
overlay.css(cssConfig);
handleResize();
$(window).resize(handleResize);
context = overlay[0].getContext('2d');
context.globalCompositeOperation = 'source-over';
fillOverlay();
// Fade in the spotlight
if (settings.animate && jQuery.support.opacity) {
overlay.animate({opacity: settings.opacity}, settings.speed, settings.easing, function () {
// Trigger the onShow callback
settings.onShow.call(this);
});
} else {
if (jQuery.support.opacity) {
overlay.css('opacity', settings.opacity);
} else {
overlay.css('filter', 'alpha(opacity=' + settings.opacity * 100 + ')');
}
// Trigger the onShow callback
settings.onShow.call(this);
}
// Set up click to close
if (settings.exitEventAppliesToElement) {
overlay.css({
pointerEvents: 'none'
});
element.on(settings.exitEvent, overlay, closeOverlay);
} else {
$(document).on(settings.exitEvent, overlay, closeOverlay);
}
// Returns the jQuery object to allow for chainability.
return this;
};
})(jQuery);

View file

@ -0,0 +1,78 @@
<div class="container about-us">
<h2>
About Us
</h2>
<div class="row">
<div class="col-sm-12 about-basic-info">
<h3>The Basics</h3>
</div>
<div class="col-sm-4">
<div class="about-basic-icon"><i class="fa fa-3x fa-calendar"></i></div>
<div class="about-basic-text">
<b> Founded</b><br>
2012
</div>
</div>
<div class="col-sm-4">
<div class="about-basic-icon"><i class="fa fa-3x fa-globe"></i></div>
<div class="about-basic-text">
<b> Location</b><br>
New York City, NY
</div>
</div>
<div class="col-sm-4">
<div class="about-basic-icon"><i class="fa fa-3x fa-users"></i></div>
<div class="about-basic-text">
<b> Worker Bees</b><br>
2
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<h3>Our Story</h3>
<p>Quay.io was originally created out of necessesity when we wanted to use Docker containers with DevTable IDE. We were using Docker containers to host and isolate server processes invoked on behalf of our users and often running their code. We started by building the Docker image dynamically whenever we spun up a new host node. The image was monolithic. It was too large, took too long to build, and was hard to manage conflicts. It was everything that Docker wasn't supposed to be. When we decided to split it up into pre-built images and host them somewhere, we noticed that there wasn't a good place to host images securely. Determined to scratch our own itch, we built Quay.io, and officially launched it as an aside in our presentation to the <a href="http://www.meetup.com/Docker-NewYorkCity/events/142142762/">Docker New York City Meetup</a> on October 2nd, 2013.</p>
<p>Since that time, our users have demanded that Quay.io become our main focus. Our customers rely on us to make sure they can store and distribute their Docker images, and we understand that solemn responsibility. Our customers have been fantastic with giving us great feedback and suggestions. We are working as hard as we can to deliver on the promise and execute our vision of what a top notch Docker registry should be. We thank you for taking this journey with us.</p>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<h3>The Team</h3>
Our team is composed of two software engineers turned entrepreneurs:
</div>
</div>
<div class="row">
<div class="col-sm-7 col-sm-offset-3 col-md-10 col-md-offset-2">
<h4>Jacob Moshenko<br>
<small>Co-Founder</small></h3>
</div>
<div class="col-sm-3 col-md-2">
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/342ea83fd68d33f90b1f06f466d533c6?s=128&d=identicon">
</div>
<div class="col-sm-7 col-md-10">
<p>Jacob graduated from The University of Michigan with a Bachelors in Computer Engineering. From there he allowed his love of flight and mountains to lure him to Seattle where he took a job with Boeing Commercial Airplanes working on the world's most accurate flight simulator. When he realized how much he also loved web development, he moved to Amazon to work on the e-commerce back-end. Finally, desiring to move to New York City, he moved to Google, where he worked on several products related to Google APIs.</p>
</div>
</div>
<div class="row">
<div class="col-sm-7 col-sm-offset-3 col-md-10 col-md-offset-2">
<h4>Joseph Schorr<br>
<small>Co-Founder</small></h3>
</div>
<div class="col-sm-3 col-md-2">
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/9fc3232622773fb2e8f71c0027601bc5?s=128&d=mm">
</div>
<div class="col-sm-7 col-md-10">
<p>Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. Joseph was one of the original duo responsible for inventing the language and framework on which DevTable is built.</p>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<p>With a combined 10 years experience building tools for software engineers, our founding team knows what it takes to make software engineers happy doing their work. Combined with our love for the web, we are ready to make a difference in the way people think about software development in the cloud.</p>
</div>
</div>
</div>

View file

@ -5,7 +5,7 @@
</h2>
<div class="row contact-options">
<div class="col-sm-3 text-center">
<div class="col-sm-3 text-center option-email">
<span class="fa-stack fa-3x text-center">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-envelope fa-stack-1x fa-inverse"></i>
@ -14,7 +14,7 @@
<h4><a href="mailto:support@quay.io">support@quay.io</a></h4>
</div>
<div class="col-sm-3 text-center">
<div class="col-sm-3 text-center option-irc">
<span class="fa-stack fa-3x">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-comment fa-stack-1x fa-inverse"></i>
@ -23,7 +23,7 @@
<h4><a href="irc://chat.freenode.net:6665/quayio">Freenode: #quayio</a></h4>
</div>
<div class="col-sm-3 text-center">
<div class="col-sm-3 text-center option-phone">
<span class="fa-stack fa-3x">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-phone fa-stack-1x fa-inverse"></i>
@ -32,7 +32,7 @@
<h4><a href="tel:+1-888-930-3475">888-930-3475</a></h4>
</div>
<div class="col-sm-3 text-center">
<div class="col-sm-3 text-center option-twitter">
<span class="fa-stack fa-3x">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="fa fa-twitter fa-stack-1x fa-inverse"></i>

View file

@ -3,7 +3,7 @@
<div class="header">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
<i class="fa fa-archive fa-lg" style="color: #aaa; margin-right: 10px;"></i>
<span class="repo-circle no-background" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo" image="image.value"></span>
</h3>
</div>

View file

@ -54,7 +54,7 @@
<label for="orgName">Organization Name</label>
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
data-placement="right">
data-placement="right" data-container="body">
<span class="description">This will also be the namespace for your repositories</span>
</div>

View file

@ -12,16 +12,7 @@
<div class="quay-spinner"></div>
</div>
<div class="container" ng-show="!user.anonymous && uploading">
<span class="message">Uploading file {{ upload_file }}</span>
<div class="progress progress-striped active">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ upload_progress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + upload_progress + '%' }}">
</div>
</div>
</div>
<div class="container new-repo" ng-show="!user.anonymous && !creating && !uploading && !building">
<div class="container new-repo" ng-show="!user.anonymous && !creating && !building">
<form method="post" name="newRepoForm" id="newRepoForm" ng-submit="createNewRepo()">
<!-- Header -->
@ -109,11 +100,9 @@
</div>
<div class="initialize-repo" ng-show="repo.initialize">
<div class="init-description">
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
</div>
<input id="file-drop" class="file-drop" type="file">
<div class="dockerfile-build-form" repository="createdForBuild" upload-failed="handleBuildFailed(message)"
build-started="handleBuildStarted()" build-failed="handleBuildFailed(message)" start-now="createdForBuild"
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
</div>
</div>
</div>
@ -123,7 +112,7 @@
<div class="col-md-1"></div>
<div class="col-md-8">
<button class="btn btn-large btn-success" type="submit"
ng-disabled="newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan))">
ng-disabled="uploading || building || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || (repo.initialize && !hasDockerfile)">
Create Repository
</button>
</div>
@ -157,26 +146,6 @@
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="missingfileModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">File required</h4>
</div>
<div class="modal-body">
A file is required in order to initialize a repository.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="cannotcreateModal">
<div class="modal-dialog">
@ -195,26 +164,6 @@
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="couldnotbuildModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot initialize repository</h4>
</div>
<div class="modal-body">
The repository could not be initialized with the selected Dockerfile. Please try again later.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="couldnotsubscribeModal">
<div class="modal-dialog">

View file

@ -25,7 +25,7 @@
<div class="plan-manager" organization="orgname" plan-changed="planChanged(plan)"></div>
</div>
<!-- Organiaztion settings tab -->
<!-- Organization settings tab -->
<div id="settings" class="tab-pane">
<div class="quay-spinner" ng-show="changingOrganization"></div>

View file

@ -1,11 +1,15 @@
<div class="container" ng-show="deleting"><div class="quay-spinner"></div></div>
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
<div class="container repo repo-admin" ng-show="accessDenied">
You do not have permission to view this page
</div>
<div class="container repo repo-admin" ng-show="repo && !deleting">
<div class="header row">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
<span class="repo-circle no-background" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo" subsection-icon="'fa-cog'"
subsection="'Admin'"></span>
</h3>
</div>
@ -88,8 +92,11 @@
</tr>
<tr>
<td colspan="2" class="admin-search">
<span class="entity-search" namespace="repo.namespace" include-teams="true" input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'" entity-selected="addNewPermission" is-organization="repo.is_organization"></span>
<td id="add-entity-permission" colspan="2" class="admin-search">
<span class="entity-search" namespace="repo.namespace" include-teams="true"
input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'"
entity-selected="addNewPermission" is-organization="repo.is_organization"
current-entity="selectedEntity"></span>
</td>
</tr>
</table>

View file

@ -1,25 +1,35 @@
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
<div class="container repo repo-build" ng-show="repo && !deleting">
<div class="container repo repo-build" ng-show="accessDenied">
You do not have permission to view this page
</div>
<div class="container repo repo-build" ng-show="repo">
<div class="header row">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
<span class="repo-circle no-background" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo" subsection-icon="'fa-tasks'" subsection="'Build History'"></span>
</h3>
<div class="repo-controls">
<button class="btn btn-success" ng-click="showNewBuildDialog()">
<i class="fa fa-plus"></i>
New Dockerfile Build
</button>
</div>
</div>
<div class="row" ng-show="!repo.is_building">
There are no active builds for this repository
<div class="row" ng-show="!builds.length">
There are no builds for this repository
</div>
<div class="row" ng-show="repo.is_building">
<div class="row" ng-show="builds.length">
<!-- Side tabs -->
<div class="col-sm-2">
<ul class="nav nav-pills nav-stacked">
<li ng-class="currentBuild == build ? 'active' : ''" ng-repeat="build in builds">
<li ng-class="currentBuild == build ? 'active' : ''" ng-repeat="build in builds | reverse">
<a class="build-tab-link" href="javascript:void(0)" ng-click="setCurrentBuild(build.id, true)">
<span class="phase-icon" ng-class="build.phase"></span>
<span>{{ getShortId(build.id) }}</span>
<span>{{ build.display_name }}</span>
</a>
</li>
</ul>
@ -35,41 +45,76 @@
Started: <span am-time-ago="build.started || 0"></span>
</div>
<span class="phase-icon" ng-class="build.phase"></span>
<span class="build-message" build="build"></span>
<span class="build-message" phase="build.phase"></span>
<div class="build-progress" build="build"></div>
</div>
<div class="build-logs">
<div ng-show="!commands">
<div ng-show="!logEntries">
<span class="quay-spinner"></span>
</div>
<div class="command" ng-repeat="command in commands">
<div class="command-entry" ng-click="toggleCommand(command)">
<i class="fa chevron" ng-class="command.expanded ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
<span bindonce>
<span class="label" bo-class="getCommandKind(command.message)" bo-show="getCommandKind(command.message)"
bo-text="getCommandKind(command.message)">
</span>
<span class="command-title" bo-html="getCommandTitleHtml(command.message)"></span>
</span>
</div>
<div class="command-logs panel-collapse collapse" ng-class="command.expanded ? 'in' : 'out'">
<div class="log-entry" bindonce ng-repeat="log in command.logs">
<span class="id" bo-text="$index + command.index + 1"></span>
<span class="message" bo-text="log.message"></span>
<div class="log-container" ng-class="container.type" ng-repeat="container in logEntries">
<div class="container-header" ng-class="container.type == 'phase' ? container.message : ''"
ng-switch on="container.type" ng-click="toggleLogs(container)">
<i class="fa chevron"
ng-class="container.logs ? 'fa-chevron-down' : 'fa-chevron-right'" ng-show="hasLogs(container)"></i>
<div ng-switch-when="phase">
<span class="container-content build-log-phase" phase="container"></span>
</div>
<div ng-show="!command.logs">
<span class="quay-spinner"></span>
<div ng-switch-when="error">
<span class="container-content build-log-error" error="container"></span>
</div>
<div ng-switch-when="command">
<span class="container-content build-log-command" command="container"></span>
</div>
</div>
<!-- Display the entries for the container -->
<div class="container-logs" ng-show="container.logs">
<div class="log-entry" bindonce ng-repeat="entry in container.logs">
<span class="id" bo-text="$index + container.index + 1"></span>
<span class="message" bo-html="processANSI(entry.message, container)"></span>
</div>
</div>
</div>
</div>
<div>
<div style="margin-top: 10px">
<span class="quay-spinner" ng-show="polling"></span>
<button class="btn" ng-show="(build.phase == 'error' || build.phase == 'complete') && build.resource_key"
ng-class="build.phase == 'error' ? 'btn-success' : 'btn-default'"
ng-click="askRestartBuild(build)">
<i class="fa fa-refresh"></i>
Run Build Again
</button>
<span class="build-id">{{ build.id }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="dockerfile-build-dialog" show-now="buildDialogShowCounter" repository="repo"
build-started="handleBuildStarted(build)">
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="confirmRestartBuildModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Run Dockerfile Build?</h4>
</div>
<div class="modal-body">
Are you sure you want to run this Dockerfile build again? The results will be immediately pushed to the repository.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="restartBuild(currentBuild)">Run Build</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -28,7 +28,10 @@
<div ng-show="user_repositories.value.length > 0">
<div class="repo-listing" ng-repeat="repository in user_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
@ -50,7 +53,10 @@
<div class="resource-view" resource="public_repositories">
<div class="repo-listing" ng-repeat="repository in public_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
<div class="page-controls">

View file

@ -27,7 +27,8 @@
<tr ng-show="canEditMembers">
<td colspan="2">
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a user...'"
entity-selected="addNewMember" is-organization="true"></span>
entity-selected="addNewMember" is-organization="true"
current-entity="selectedMember"></span>
</td>
</tr>
</table>

View file

@ -0,0 +1,3 @@
<div class="container">
<div class="angular-tour-ui" tour="tour" inline="true"></div>
</div>

View file

@ -5,51 +5,65 @@
</div>
<div class="resource-view" resource="repository" error-message="'No Repository Found'">
<div class="container repo">
<div class="container repo repo-view">
<!-- Repo Header -->
<div class="header">
<h3>
<span class="repo-circle" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo"></span>
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="bottom">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
<i class="fa fa-cog fa-lg"></i>
</a>
</span>
</h3>
<!-- Pull command -->
<div class="pull-command visible-md visible-lg" style="display: none;">
<span class="pull-command-title">Pull repository:</span>
<div class="pull-container">
<div class="input-group">
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
<i class="fa fa-copy"></i>
</span>
</div>
</div>
<div id="clipboardCopied" class="hovering" style="display: none">
Copied to clipboard
</div>
</div>
</div>
<!-- Status boxes -->
<div class="status-boxes">
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building">
<div class="dropdown" data-placement="top">
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">Building Dockerfile<span ng-show="buildsInfo.length > 1">s</span> <b class="caret"></b></a>
<ul class="dropdown-menu pull-right">
<div class="repo-controls">
<!-- Builds -->
<div class="dropdown" data-placement="top" style="display: inline-block"
bs-tooltip="buildsInfo ? 'Dockerfile Builds Running: ' + (buildsInfo.length) : 'Dockerfile Build'"
ng-show="repo.can_write || buildsInfo.length">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-tasks fa-lg"></i>
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '' }}</span></span>
<b class="caret"></b>
</button>
<ul class="dropdown-menu">
<li ng-show="repo.can_write"><a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/build' }}">
<i class="fa fa-tasks"></i>Dockerfile Build History</a>
</li>
<li ng-show="repo.can_write">
<a href="javascript:void(0)" ng-click="showNewBuildDialog()">
<i class="fa fa-plus" style="margin-left: 1px; margin-right: 8px;"></i>New Dockerfile Build
</a>
</li>
<li role="presentation" class="divider" ng-show="buildsInfo && repo.can_write"></li>
<li role="presentation" class="dropdown-header" ng-show="buildsInfo">Current Builds</li>
<li ng-repeat="buildInfo in buildsInfo">
<div class="build-info" ng-class="repo.can_write ? 'clickable' : ''" ng-click="showBuild(buildInfo)">
<div class="build-info" ng-class="repo.can_write ? 'clickable' : ''" ng-click="repo.can_write && showBuild(buildInfo)">
<span class="build-status" build="buildInfo"></span>
</div>
</li>
</ul>
</div>
<!-- Admin -->
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}"
ng-show="repo.can_admin">
<button class="btn btn-default" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="top">
<i class="fa fa-cog fa-lg"></i></button></a>
<!-- Pull Command -->
<span class="pull-command visible-md-inline">
<div class="pull-container" title="Pull repository" bs-tooltip="tooltip.title">
<div class="input-group">
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
<i class="fa fa-copy"></i>
</span>
</div>
</div>
<div id="clipboardCopied" class="hovering" style="display: none">
Copied to clipboard
</div>
</span>
</div>
</div>
@ -72,7 +86,7 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<div class="repo-content" ng-show="!currentTag.image && repo.is_building">
<div class="empty-message">
Your build is currently processing, if this takes longer than an hour, please contact <a href="mailto:support@quay.io">Quay.io support</a>
A build is currently processing. If this takes longer than an hour, please <a href="/contact">contact us</a>
</div>
</div>
@ -93,10 +107,10 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
<!-- Side Panel -->
<div class="col-md-4">
<div class="panel panel-default">
<div id="side-panel" class="panel panel-default">
<div class="panel-heading">
<!-- Dropdown -->
<div class="tag-dropdown dropdown" data-placement="top">
<div id="side-panel-dropdown" class="tag-dropdown dropdown" data-placement="top">
<i class="fa fa-tag" ng-show="currentTag"></i>
<i class="fa fa-archive" ng-show="!currentTag"></i>
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag ? currentTag.name : currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
@ -235,6 +249,10 @@ sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</div>
</div>
<div class="dockerfile-build-dialog" show-now="buildDialogShowCounter" repository="repo"
build-started="handleBuildStarted(build)">
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteTagModal">
<div class="modal-dialog">

View file

@ -11,12 +11,20 @@
</url>
<url>
<loc>https://quay.io/organizations/</loc>
<changefreq>monthly</changefreq>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://quay.io/repository/</loc>
<changefreq>always</changefreq>
</url>
<url>
<loc>https://quay.io/contact/</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://quay.io/about/</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://quay.io/tos</loc>
<changefreq>monthly</changefreq>

View file

@ -0,0 +1,24 @@
<p>The first step to creating an image is to create a container and fill it with some data.</p>
<p>First we'll create a container with a single new file based off of the <code>ubuntu</code> base image:</p>
<pre class="command">
<code ng-show="tour.tourScope.showSudo">sudo </code>docker run busybox echo "fun" > newfile
</pre>
<p>The container will immediately terminate (because its one command is <code>echo</code>), so we'll use <code>docker ps -l</code> to list it:
<pre class="command">
<code ng-show="tour.tourScope.showSudo">sudo </code>docker ps -l
CONTAINER ID IMAGE COMMAND CREATED
<var class="var1">07f2065197ef</var> ubuntu:12.04 echo fun 31 seconds ago
</pre>
<div class="alert alert-info">
<div class="control-group">
<label class="control-label" for="containerId">Enter the container ID returned:</label>
<div class="form-inline">
<input type="text" id="containerId" class="form-control" placeholder="containerId" ng-model="tour.tourScope.containerId">
</div>
</div>
</div>

View file

@ -0,0 +1,22 @@
<div class="alert alert-info">
<div class="control-group">
<label class="control-label" for="containerId">Enter a repository name:</label>
<div class="form-inline">
<form name="repoNameForm">
<input type="text" id="repoName" name="repoName" class="form-control"
ng-model="tour.tourScope.repoName" ng-pattern="/^[a-z0-9_-]+$/">
</form>
</div>
<span ng-show="!repoNameForm.repoName.$valid" style="margin-left: 10px; margin-top: 7px;">
Repository names must match [a-z0-9_-]+
</span>
</div>
</div>
<p>Once a container has terminated in Docker, the next step is to <i>commit</i> the container to an image, and then <i>tag</i> that image with a relevant name so it can be saved to a repository.</p>
<p>Docker lets us do this in one step with the <i>commit</i> command. To do so, we run the <code>docker commit</code> with the container ID from the previous step and tag it to be a repository under <code>quay.io</code>.
<pre class="command">
<code ng-show="tour.tourScope.showSudo">sudo </code>docker commit <var class="var1">{{ tour.tourScope.containerId || 'containerId' }}</var> quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>
</pre>

View file

@ -0,0 +1,17 @@
<div class="container">
<div class="alert alert-info" style="padding: 4px;">
<div class="checkbox">
<label>
<input name="showSudo" type="checkbox" ng-model="tour.tourScope.showSudo" style="display: inline-block; margin-left: 0px; margin-right: 10px;">
My OS requires me to run all <code>docker</code> commands with <code>sudo</code>
</label>
</div>
</div>
<p>The first step when using Quay.io is to login via the <code>docker login</code> command.</p>
<p>Enter your Quay.io username and your password when prompted.</p>
<pre class="command"><code ng-show="tour.tourScope.showSudo">sudo </code>docker login quay.io
Username: {{ tour.tourScope.username }}
Password: (password here)
Email: {{ tour.tourScope.email }}</pre>
</div>

View file

@ -0,0 +1 @@
That's it for the introduction tutorial! If you have any questions, please check the <a href="http://docs.quay.io" target="_blank">Quay Documentation</a> or <a href="/contact">contact us</a>!

View file

@ -0,0 +1,2 @@
<p>The permissions tab displays all the users, robot accounts and tokens that have access to the repository.</p>
<p>By default, any repository you create will have your user ({{ tour.tourScope.username }}) as the single administrator.</p>

View file

@ -0,0 +1,8 @@
<p>Now that we've tagged our image with a repository name, we can <code>push</code> the repository to Quay.io:</p>
<pre class="command">
<code ng-show="tour.tourScope.showSudo">sudo </code>docker push quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>
The push refers to a repository [quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var>] (len: 1)
Sending image list
Pushing repository quay.io/{{ tour.tourScope.username }}/<var class="var2">{{ tour.tourScope.repoName || 'myfirstrepo' }}</var> (1 tags)
</pre>

View file

@ -0,0 +1 @@
Your repository <span class="repo-name-small">{{ tour.tourScope.username }}/{{ tour.tourScope.repoName }}</span> is currently being pushed to Quay.io...

View file

@ -0,0 +1,3 @@
<p>This page displays all your personal repositories, as well as select public repositories</p>
<p>To view your new repository, click on <strong>{{ tour.tourScope.username }}/{{ tour.tourScope.repoName }}</strong></p>

View file

@ -0,0 +1,11 @@
<div class="container signin-container">
<div class="row" style="margin-bottom: 10px">
This tutorial will interact with your account, so please sign in to get started
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="user-setup" redirect-url="'/tutorial/'"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<p>Your repository <span class="repo-name-small">{{ tour.tourScope.username }}/{{ tour.tourScope.repoName }}</span> has been pushed to Quay.io!</p>
<p>To view your repository, first click on the <strong>Repositories</strong> link in the header to continue. This will display the repositories list.</p>

View file

@ -0,0 +1,7 @@
<p>
Waiting for your repository to be listed.
</p>
<p ng-show="tour.tourScope.inOrganization">
Please make sure the <strong>{{ tour.tourScope.username }}</strong> namespace is selected.
</p>

View file

@ -0,0 +1,12 @@
<p>This tutorial will walk you step-by-step through using Quay.io, covering:
<ul>
<li>Logging into Quay.io from Docker</li>
<li>Starting a container</li>
<li>Creating images from a container</li>
<li>Pushing a repository to Quay.io</li>
<li>Viewing a repository</li>
<li>Changing a repository's permissions</li>
</ul>
</p>
<p>This tutorial should take about <b>15 minutes</b>, but may take slightly longer depending on the speed of your internet connection</p>

View file

@ -38,14 +38,12 @@
<script src="//code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.0.0/bootbox.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-sanitize.min.js"></script>
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
<script src="//cdn.jsdelivr.net/restangular/1.2.0/restangular.min.js"></script>
<script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0"></script>
<script src="static/lib/loading-bar.js"></script>
<script src="static/lib/angular-strap.min.js"></script>
@ -54,6 +52,7 @@
<script src="static/lib/angulartics-google-analytics.js"></script>
<script src="static/lib/angular-md5.js"></script>
<script src="static/lib/bindonce.min.js"></script>
<script src="static/lib/ansi2html.js"></script>
<script src="static/lib/angular-moment.min.js"></script>
<script src="static/lib/angular-cookies.min.js"></script>
@ -73,6 +72,7 @@
window.__token = '{{ csrf_token() }}';
</script>
<script src="static/js/tour.js"></script>
<script src="static/js/app.js"></script>
<script src="static/js/controllers.js"></script>
<script src="static/js/graphing.js"></script>
@ -117,10 +117,11 @@ var isProd = document.location.hostname === 'quay.io';
<ul>
<li><span class="copyright">&copy;2014 DevTable, LLC</span></li>
<li><a href="http://blog.devtable.com/">Blog</a></li>
<li><a href="/tos" target="_self">Terms of Service</a></li>
<li><a href="/privacy" target="_self">Privacy Policy</a></li>
<li><a href="/tos" target="_self">Terms</a></li>
<li><a href="/privacy" target="_self">Privacy</a></li>
<li><a href="/security/">Security</a></li>
<li><b><a href="/contact/">Contact Us</a></b></li>
<li><a href="/about/">About</a></li>
<li><b><a href="/contact/">Contact</a></b></li>
</ul>
</div>
@ -171,5 +172,6 @@ var isProd = document.location.hostname === 'quay.io';
{% endif %}
<!-- end olark code -->
<div class="angular-tour-ui" inline="false" tour="angular_tour_current"></div>
</body>
</html>

View file

@ -35,6 +35,7 @@
<script src="static/lib/Blob.js"></script>
<script src="static/lib/FileSaver.js"></script>
<script src="static/lib/jquery.base64.min.js"></script>
<script src="static/lib/jquery.spotlight.js"></script>
{% endblock %}
{% block body_content %}

Binary file not shown.

View file

@ -231,9 +231,9 @@ def build_specs():
TestSpec(url_for('api.get_webhook', repository=PUBLIC_REPO,
public_id=FAKE_WEBHOOK), admin_code=403),
TestSpec(url_for('api.get_webhook', repository=ORG_REPO,
public_id=FAKE_WEBHOOK), admin_code=400),
public_id=FAKE_WEBHOOK), admin_code=404),
TestSpec(url_for('api.get_webhook', repository=PRIVATE_REPO,
public_id=FAKE_WEBHOOK), admin_code=400),
public_id=FAKE_WEBHOOK), admin_code=404),
TestSpec(url_for('api.list_webhooks', repository=PUBLIC_REPO),
admin_code=403),
@ -382,9 +382,9 @@ def build_specs():
TestSpec(url_for('api.get_tokens', repository=PUBLIC_REPO,
code=FAKE_TOKEN), admin_code=403),
TestSpec(url_for('api.get_tokens', repository=ORG_REPO, code=FAKE_TOKEN),
admin_code=400),
admin_code=404),
TestSpec(url_for('api.get_tokens', repository=PRIVATE_REPO,
code=FAKE_TOKEN), admin_code=400),
code=FAKE_TOKEN), admin_code=404),
TestSpec(url_for('api.create_token', repository=PUBLIC_REPO),
admin_code=403).set_method('POST'),
@ -587,13 +587,4 @@ def build_index_specs():
IndexTestSpec(url_for('tags.delete_tag', repository=ORG_REPO,
tag=FAKE_TAG_NAME),
NO_REPO, 403, 403, 403, 400).set_method('DELETE'),
IndexTestSpec(url_for('tags.delete_repository_tags',
repository=PUBLIC_REPO),
NO_REPO, 403, 403, 403, 403).set_method('DELETE'),
IndexTestSpec(url_for('tags.delete_repository_tags',
repository=PRIVATE_REPO),
NO_REPO, 403, 403, 403, 204).set_method('DELETE'),
IndexTestSpec(url_for('tags.delete_repository_tags', repository=ORG_REPO),
NO_REPO, 403, 403, 403, 204).set_method('DELETE'),
]

1336
test/test_api_usage.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,151 +1,194 @@
import math
import logging
from random import SystemRandom
from loremipsum import get_sentence
from functools import wraps
from copy import deepcopy
from data.buildlogs import BuildLogs
from random import choice
logger = logging.getLogger(__name__)
random = SystemRandom()
def maybe_advance_script(is_get_status=False):
def inner_advance(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
advance_units = random.randint(1, 500)
logger.debug('Advancing script %s units', advance_units)
while advance_units > 0 and self.remaining_script:
units = self.remaining_script[0][0]
if advance_units > units:
advance_units -= units
self.advance_script(is_get_status)
else:
break
return func(self, *args, **kwargs)
return wrapper
return inner_advance
class TestBuildLogs(BuildLogs):
COMMAND_TYPES = ['FROM', 'MAINTAINER', 'RUN', 'CMD', 'EXPOSE', 'ENV', 'ADD',
'ENTRYPOINT', 'VOLUME', 'USER', 'WORKDIR']
STATUS_TEMPLATE = {
'total_commands': None,
'current_command': None,
'push_completion': 0.0,
'image_completion': {},
}
def __init__(self, redis_host, namespace, repository, test_build_id):
super(TestBuildLogs, self).__init__(redis_host)
self.namespace = namespace
self.repository = repository
self.test_build_id = test_build_id
self.last_command = 0
self.logs = [self._generate_command()]
self.commands = [{
'index': 0,
'message': self.logs[0]['message'],
}]
self.request_counter = 0
self._generate_logs()
self.remaining_script = self._generate_script()
logger.debug('Total script size: %s', len(self.remaining_script))
self._logs = []
self._status = {}
self._last_status = {}
def _get_random_command(self):
COMMANDS = ['FROM', 'MAINTAINER', 'RUN', 'CMD', 'EXPOSE', 'ENV', 'ADD',
'ENTRYPOINT', 'VOLUME', 'USER', 'WORKDIR']
def advance_script(self, is_get_status):
(_, log, status_wrapper) = self.remaining_script.pop(0)
if log is not None:
self._logs.append(log)
return choice(COMMANDS)
if status_wrapper is not None:
(phase, status) = status_wrapper
def _generate_command(self):
self.last_command += 1
from data import model
build_obj = model.get_repository_build(self.namespace, self.repository,
self.test_build_id)
build_obj.phase = phase
build_obj.save()
self._status = status
if not is_get_status:
self._last_status = status
def _generate_script(self):
script = []
# generate the init phase
script.append(self._generate_phase(400, 'initializing'))
script.extend(self._generate_logs(random.randint(1, 3)))
# move to the building phase
script.append(self._generate_phase(400, 'building'))
total_commands = random.randint(5, 20)
for command_num in range(1, total_commands + 1):
command_weight = random.randint(50, 100)
script.append(self._generate_command(command_num, total_commands,
command_weight))
# we want 0 logs some percent of the time
num_logs = max(0, random.randint(-50, 400))
script.extend(self._generate_logs(num_logs))
# move to the pushing phase
script.append(self._generate_phase(400, 'pushing'))
script.extend(self._generate_push_statuses(total_commands))
# move to the error or complete phase
if random.randint(0, 1) == 0:
script.append(self._generate_phase(400, 'complete'))
else:
script.append(self._generate_phase(400, 'error'))
script.append((1, {'message': 'Something bad happened! Oh noes!',
'type': self.ERROR}, None))
return script
def _generate_phase(self, start_weight, phase_name):
return (start_weight, {'message': phase_name, 'type': self.PHASE},
(phase_name, deepcopy(self.STATUS_TEMPLATE)))
def _generate_command(self, command_num, total_commands, command_weight):
sentence = get_sentence()
command = self._get_random_command()
command = random.choice(self.COMMAND_TYPES)
if command == 'FROM':
sentence = choice(['ubuntu', 'quay.io/devtable/simple', 'quay.io/buynlarge/orgrepo', 'stackbrew/ubuntu:precise'])
sentence = random.choice(['ubuntu', 'lopter/raring-base',
'quay.io/devtable/simple',
'quay.io/buynlarge/orgrepo',
'stackbrew/ubuntu:precise'])
return {
'message': 'Step %s: %s %s' % (self.last_command, command, sentence),
'is_command': True,
msg = {
'message': 'Step %s: %s %s' % (command_num, command, sentence),
'type': self.COMMAND,
}
def _generate_logs(self):
rand = SystemRandom()
num_logs = rand.randint(1, 500)
for _ in range(num_logs):
if rand.randint(1, 50) == 1:
cmd = self._generate_command()
self.commands.append({
'message': cmd['message'],
'index': len(self.logs),
})
self.logs.append(cmd)
else:
self.logs.append({
'message': get_sentence(),
})
status = deepcopy(self.STATUS_TEMPLATE)
status['total_commands'] = total_commands
status['current_command'] = command_num
return (command_weight, msg, ('building', status))
@staticmethod
def _generate_image_completion(rand_func):
images = {}
for image_id in range(rand_func.randint(1, 11)):
total = int(math.pow(abs(rand_func.gauss(0, 1000)), 2))
current = rand_func.randint(0, total)
image_id = 'image_id_%s' % image_id
images[image_id] = {
'total': total,
'current': current,
def _generate_logs(count):
others = []
if random.randint(0, 10) <= 8:
count = count - 2
others = [(1, {'message': '\x1b[91m' + get_sentence()}, None), (1, {'message': '\x1b[0m'}, None)]
return others + [(1, {'message': get_sentence()}, None) for _ in range(count)]
@staticmethod
def _compute_total_completion(statuses, total_images):
percentage_with_sizes = float(len(statuses.values()))/total_images
sent_bytes = sum([status[u'current'] for status in statuses.values()])
total_bytes = sum([status[u'total'] for status in statuses.values()])
return float(sent_bytes)/total_bytes*percentage_with_sizes
@staticmethod
def _generate_push_statuses(total_commands):
push_status_template = deepcopy(TestBuildLogs.STATUS_TEMPLATE)
push_status_template['current_command'] = total_commands
push_status_template['total_commands'] = total_commands
push_statuses = []
one_mb = 1 * 1024 * 1024
num_images = random.randint(2, 7)
sizes = [random.randint(one_mb, one_mb * 5) for _ in range(num_images)]
image_completion = {}
for image_num, image_size in enumerate(sizes):
image_id = 'image_id_%s' % image_num
image_completion[image_id] = {
'current': 0,
'total': image_size,
}
return images
def _generate_fake_status(self):
random = SystemRandom()
phases = {
'waiting': {},
'starting': {
'total_commands': 7,
'current_command': 0,
},
'initializing': {},
'error': {},
'complete': {},
'building': {
'total_commands': 7,
'current_command': random.randint(1, 7),
},
'pushing': {
'total_commands': 7,
'current_command': 7,
'push_completion': random.random(),
'image_completion': self._generate_image_completion(random),
},
}
for i in range(one_mb, image_size, one_mb):
image_completion[image_id]['current'] = i
new_status = deepcopy(push_status_template)
new_status['image_completion'] = deepcopy(image_completion)
phase = random.choice(phases.keys())
completion = TestBuildLogs._compute_total_completion(image_completion,
num_images)
new_status['push_completion'] = completion
push_statuses.append((250, None, ('pushing', new_status)))
from data import model
build_obj = model.get_repository_build(self.namespace, self.repository,
self.test_build_id)
build_obj.phase = phase
build_obj.save()
return push_statuses
return phases[phase]
def get_log_entries(self, build_id, start_index, end_index):
@maybe_advance_script()
def get_log_entries(self, build_id, start_index):
if build_id == self.test_build_id:
self.request_counter += 1
if self.request_counter % 10 == 0:
self._generate_logs()
logger.debug('Returning logs %s:%s', start_index, end_index)
if end_index >= 0:
end_index += 1
return (len(self.logs), self.logs[start_index:end_index])
return (len(self._logs), self._logs[start_index:])
else:
return super(TestBuildLogs, self).get_log_entries(build_id, start_index,
end_index)
def get_commands(self, build_id):
if build_id == self.test_build_id:
self.request_counter += 1
if self.request_counter % 10 == 0:
self._generate_logs()
return self.commands
else:
return super(TestBuildLogs, self).get_commands(build_id)
def get_last_command(self, build_id):
if build_id == self.test_build_id:
self.request_counter += 1
if self.request_counter % 10 == 0:
self._generate_logs()
return self.commands[-1]
else:
return super(TestBuildLogs, self).get_last_command(build_id)
return super(TestBuildLogs, self).get_log_entries(build_id, start_index)
@maybe_advance_script(True)
def get_status(self, build_id):
if build_id == self.test_build_id:
self.request_counter += 1
if self.request_counter % 10 == 0:
self._generate_logs()
last_status = self._status
self._status = self._generate_fake_status()
return last_status
returnable_status = self._last_status
self._last_status = self._status
return returnable_status
else:
return super(TestBuildLogs, self).get_status(build_id)

View file

@ -35,3 +35,6 @@ class FakeUserfiles(object):
def get_file_url(self, file_id, expires_in=300):
return ('http://fake/url')
def get_file_checksum(self, file_id):
return 'abcdefg'

48
tools/audittagimages.py Normal file
View file

@ -0,0 +1,48 @@
from data.database import Image, RepositoryTag, Repository
from app import app
store = app.config['STORAGE']
tag_query = (RepositoryTag
.select(RepositoryTag, Image, Repository)
.join(Repository)
.switch(RepositoryTag)
.join(Image))
for tag in tag_query:
if tag.image.repository.id != tag.repository.id:
print('Repository tag pointing to external image: %s/%s:%s' %
(tag.repository.namespace, tag.repository.name, tag.name))
proper_image_layer_path = store.image_layer_path(tag.repository.namespace,
tag.repository.name,
tag.image.docker_image_id)
has_storage = False
if store.exists(proper_image_layer_path):
print('Storage already in place: %s' % proper_image_layer_path)
has_storage = True
else:
print('Storage missing: %s' % proper_image_layer_path)
has_db_entry = False
new_image = None
try:
new_image = Image.get(Image.docker_image_id == tag.image.docker_image_id,
Image.repository == tag.repository)
has_db_entry = True
print('DB image in place: %s invalid image id: %s' % (new_image.id,
tag.image.id))
except Image.DoesNotExist:
print('DB image missing: %s' % tag.image.docker_image_id)
if has_storage and has_db_entry:
print('Switching tag to proper image %s/%s/%s -> %s' %
(tag.repository.namespace, tag.repository.name, tag.name,
new_image.id))
tag.image = new_image
tag.save()
print('Done')

39
workers/README.md Normal file
View file

@ -0,0 +1,39 @@
to prepare a new build node host:
```
sudo apt-get update
sudo apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev gdebi-core
```
check out the code, install the kernel, custom docker, nsexec, and reboot:
```
git clone https://bitbucket.org/yackob03/quay.git
cd quay
sudo gdebi --n binary_dependencies/builder/linux-headers-3.11.0-17_3.11.0-17.28_all.deb
sudo gdebi --n binary_dependencies/builder/linux-headers-3.11.0-17-generic_3.11.0-17.28_amd64.deb
sudo gdebi --n binary_dependencies/builder/linux-image-3.11.0-17-generic_3.11.0-17.28_amd64.deb
sudo gdebi --n binary_dependencies/builder/linux-image-extra-3.11.0-17-generic_3.11.0-17.28_amd64.deb
sudo gdebi --n binary_dependencies/builder/nsexec_1.22ubuntu1trusty1_amd64.deb
sudo gdebi --n binary_dependencies/builder/lxc-docker-0.8.0-tutum_0.8.0-tutum-20140212002736-afad5c0-dirty_amd64.deb
sudo chown -R 100000:100000 /var/lib/docker
sudo shutdown -r now
```
pull some base images if you want (optional)
```
sudo docker pull ubuntu
sudo docker pull stackbrew/ubuntu
sudo docker pull busybox
sudo docker pull lopter/raring-base
```
start the worker
```
cd quay
virtualenv --distribute venv
source venv/bin/activate
pip install -r requirements.txt
sudo STACK=prod venv/bin/python -m workers.dockerfilebuild -D
```

View file

@ -10,6 +10,7 @@ import shutil
from docker import Client, APIError
from tempfile import TemporaryFile, mkdtemp
from zipfile import ZipFile
from functools import partial
from data.queue import dockerfile_build_queue
from data import model
@ -53,9 +54,9 @@ class DockerfileBuildContext(object):
self._build_dir = build_context_dir
self._tag_name = tag_name
self._push_token = push_token
self._build_uuid = build_uuid
self._cl = Client(timeout=1200, version='1.7')
self._status = StatusWrapper(self._build_uuid)
self._status = StatusWrapper(build_uuid)
self._build_logger = partial(build_logs.append_log_message, build_uuid)
dockerfile_path = os.path.join(self._build_dir, "Dockerfile")
self._num_steps = DockerfileBuildContext.__count_steps(dockerfile_path)
@ -93,24 +94,25 @@ class DockerfileBuildContext(object):
with self._status as status:
status['total_commands'] = self._num_steps
logger.debug('Building to tag names: %s' % self._tag_name)
logger.debug('Building to tag named: %s' % self._tag_name)
build_status = self._cl.build(path=self._build_dir, tag=self._tag_name,
stream=True)
current_step = 0
built_image = None
for status in build_status:
logger.debug('Status: %s', str(status.encode('utf-8')))
status_str = str(status.encode('utf-8'))
logger.debug('Status: %s', status_str)
step_increment = re.search(r'Step ([0-9]+) :', status)
if step_increment:
build_logs.append_command_message(str(status))
self._build_logger(status_str, build_logs.COMMAND)
current_step = int(step_increment.group(1))
logger.debug('Step now: %s/%s' % (current_step, self._num_steps))
with self._status as status:
status['current_command'] = current_step
continue
else:
build_logs.append_log_message(self._build_uuid, str(status))
self._build_logger(status_str)
complete = re.match(r'Successfully built ([a-z0-9]+)$', status)
if complete:
@ -260,10 +262,15 @@ class DockerfileBuildWorker(Worker):
tag_name = repository_build.tag
access_token = repository_build.access_token.code
start_msg = ('Starting job with resource url: %s tag: %s and token: %s' %
(resource_url, tag_name, access_token))
log_appender = partial(build_logs.append_log_message,
repository_build.uuid)
log_appender('initializing', build_logs.PHASE)
start_msg = ('Starting job with resource url: %s tag: %s' % (resource_url,
tag_name))
logger.debug(start_msg)
build_logs.append_log_message(repository_build.uuid, start_msg)
log_appender(start_msg)
docker_resource = requests.get(resource_url)
c_type = docker_resource.headers['content-type']
@ -271,13 +278,13 @@ class DockerfileBuildWorker(Worker):
filetype_msg = ('Request to build file of type: %s with tag: %s' %
(c_type, tag_name))
logger.info(filetype_msg)
build_logs.append_log_message(repository_build.uuid, filetype_msg)
log_appender(filetype_msg)
if c_type not in self._mime_processors:
raise RuntimeError('Invalid dockerfile content type: %s' % c_type)
build_dir = self._mime_processors[c_type](docker_resource)
uuid = repository_build.uuid
log_appender('building', build_logs.PHASE)
repository_build.phase = 'building'
repository_build.save()
@ -287,24 +294,28 @@ class DockerfileBuildWorker(Worker):
built_image = build_ctxt.build()
if not built_image:
log_appender('error', build_logs.PHASE)
repository_build.phase = 'error'
repository_build.save()
build_logs.append_log_message(uuid, 'Unable to build dockerfile.')
log_appender('Unable to build dockerfile.', build_logs.ERROR)
return False
log_appender('pushing', build_logs.PHASE)
repository_build.phase = 'pushing'
repository_build.save()
build_ctxt.push(built_image)
log_appender('complete', build_logs.PHASE)
repository_build.phase = 'complete'
repository_build.save()
except Exception as exc:
log_appender('error', build_logs.PHASE)
logger.exception('Exception when processing request.')
repository_build.phase = 'error'
repository_build.save()
build_logs.append_log_message(uuid, exc.message)
log_appender(str(exc), build_logs.ERROR)
return False
return True