Merge branch 'master' into tutorial

Conflicts:
	endpoints/index.py
	static/css/quay.css
	static/js/app.js
	static/js/controllers.js
	test/data/test.db
This commit is contained in:
Joseph Schorr 2014-02-06 21:23:27 -05:00
commit 98e57b9d2b
19 changed files with 1615 additions and 66 deletions

View file

@ -4,7 +4,6 @@ import os
from app import app as application
from data.model import db as model_db
# Initialize logging
application.config['LOGGING_CONFIG']()

View file

@ -16,7 +16,7 @@ from test import analytics as fake_analytics
class FlaskConfig(object):
SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884'
JSONIFY_PRETTYPRINT_REGULAR = False
class FlaskProdConfig(FlaskConfig):
SESSION_COOKIE_SECURE = True

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):
@ -970,6 +973,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 +1059,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)

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']
@ -1278,7 +1285,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
@ -1673,7 +1684,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
@ -1810,6 +1825,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

@ -255,6 +255,8 @@ def update_images(namespace, repository):
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:
@ -271,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))

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

@ -94,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

@ -2899,3 +2899,35 @@ pre.command:before {
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;
}

View file

@ -427,6 +427,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
var planService = {};
var listeners = [];
var previousSubscribeFailure = false;
planService.getFreePlan = function() {
return 'free';
};
@ -616,12 +618,15 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
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);
});
@ -784,11 +789,12 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
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',

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,9 +48,6 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
};
}
function GuideCtrl($scope) {
}
function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
$scope.tour = {
'title': 'Quay.io Tutorial',
@ -178,12 +181,6 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) {
};
}
function SecurityCtrl($scope) {
}
function ContactCtrl($scope) {
}
function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
$scope.namespace = null;
$scope.page = 1;
@ -756,6 +753,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.permissions = {'team': [], 'user': []};
$scope.logsShown = 0;
$scope.deleting = false;
$scope.permissionCache = {};
$scope.buildEntityForPermission = function(name, permission, kind) {

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

@ -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

@ -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

@ -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>

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'),
]

1337
test/test_api_usage.py Normal file

File diff suppressed because it is too large Load diff

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')