Merge branch 'master' into looksirdroids

This commit is contained in:
Joseph Schorr 2013-11-22 18:22:29 -05:00
commit 43f2dd80a0
38 changed files with 752 additions and 400 deletions

View file

@ -4,7 +4,7 @@ to prepare a new host:
sudo apt-get install software-properties-common sudo apt-get install software-properties-common
sudo apt-add-repository -y ppa:nginx/stable sudo apt-add-repository -y ppa:nginx/stable
sudo apt-get update sudo apt-get update
sudo apt-get install -y git python-virtualenv python-dev phantomjs sudo apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev
sudo apt-get install -y nginx-full sudo apt-get install -y nginx-full
``` ```
@ -21,12 +21,9 @@ running:
``` ```
sudo nginx -c `pwd`/nginx.conf sudo nginx -c `pwd`/nginx.conf
STACK=prod gunicorn -D --workers 4 -b unix:/tmp/gunicorn.sock --worker-class eventlet -t 2000 application:application STACK=prod gunicorn -D --workers 4 -b unix:/tmp/gunicorn.sock --worker-class gevent -t 2000 application:application
``` ```
set up the snapshot script:
(instructions in the seo-snapshots directory)[seo-snapshots/README.md]
start the workers: start the workers:
``` ```

View file

@ -1296,7 +1296,7 @@ def subscription_view(stripe_subscription, used_repos):
@api_login_required @api_login_required
def get_user_card_api(): def get_user_card_api():
user = current_user.db_user() user = current_user.db_user()
return jsonify(get_card(user)) return get_card(user)
@app.route('/api/organization/<orgname>/card', methods=['GET']) @app.route('/api/organization/<orgname>/card', methods=['GET'])
@ -1305,7 +1305,7 @@ def get_org_card_api(orgname):
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
organization = model.get_organization(orgname) organization = model.get_organization(orgname)
return jsonify(get_card(organization)) return get_card(organization)
abort(403) abort(403)
@ -1315,7 +1315,7 @@ def get_org_card_api(orgname):
def set_user_card_api(): def set_user_card_api():
user = current_user.db_user() user = current_user.db_user()
token = request.get_json()['token'] token = request.get_json()['token']
return jsonify(set_card(user, token)) return set_card(user, token)
@app.route('/api/organization/<orgname>/card', methods=['POST']) @app.route('/api/organization/<orgname>/card', methods=['POST'])
@ -1331,13 +1331,14 @@ def set_org_card_api(orgname):
def set_card(user, token): def set_card(user, token):
print token
if user.stripe_id: if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id) cus = stripe.Customer.retrieve(user.stripe_id)
if cus: if cus:
cus.card = token try:
cus.save() cus.card = token
cus.save()
except stripe.CardError as e:
return carderror_response(e)
return get_card(user) return get_card(user)
@ -1364,7 +1365,7 @@ def get_card(user):
'last4': card.last4 'last4': card.last4
} }
return {'card': card_info} return jsonify({'card': card_info})
@app.route('/api/user/plan', methods=['PUT']) @app.route('/api/user/plan', methods=['PUT'])
@api_login_required @api_login_required
@ -1376,6 +1377,14 @@ def subscribe_api():
return subscribe(user, plan, token, USER_PLANS) return subscribe(user, plan, token, USER_PLANS)
def carderror_response(e):
resp = jsonify({
'carderror': e.message,
})
resp.status_code = 402
return resp
def subscribe(user, plan, token, accepted_plans): def subscribe(user, plan, token, accepted_plans):
plan_found = None plan_found = None
for plan_obj in accepted_plans: for plan_obj in accepted_plans:
@ -1400,9 +1409,13 @@ def subscribe(user, plan, token, accepted_plans):
# They want a real paying plan, create the customer and plan # They want a real paying plan, create the customer and plan
# simultaneously # simultaneously
card = token card = token
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id try:
user.save() cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id
user.save()
except stripe.CardError as e:
return carderror_response(e)
response_json = subscription_view(cus.subscription, private_repos) response_json = subscription_view(cus.subscription, private_repos)
status_code = 201 status_code = 201
@ -1418,12 +1431,17 @@ def subscribe(user, plan, token, accepted_plans):
cus.save() cus.save()
else: else:
cus.plan = plan
# User may have been a previous customer who is resubscribing # User may have been a previous customer who is resubscribing
if token: if token:
cus.card = token cus.card = token
cus.save() cus.plan = plan
try:
cus.save()
except stripe.CardError as e:
return carderror_response(e)
response_json = subscription_view(cus.subscription, private_repos) response_json = subscription_view(cus.subscription, private_repos)
resp = jsonify(response_json) resp = jsonify(response_json)

View file

@ -1,14 +1,19 @@
import logging import logging
import requests import requests
import stripe
from flask import (abort, redirect, request, url_for, render_template, from flask import (abort, redirect, request, url_for, render_template,
make_response) make_response, Response)
from flask.ext.login import login_user, UserMixin, login_required from flask.ext.login import login_user, UserMixin
from flask.ext.principal import identity_changed, Identity, AnonymousIdentity from flask.ext.principal import identity_changed
from urlparse import urlparse
from data import model from data import model
from app import app, login_manager, mixpanel from app import app, login_manager, mixpanel
from auth.permissions import QuayDeferredPermissionUser from auth.permissions import (QuayDeferredPermissionUser,
AdministerOrganizationPermission)
from util.invoice import renderInvoiceToPdf
from util.seo import render_snapshot
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -48,6 +53,19 @@ def index(path):
return render_template('index.html') return render_template('index.html')
@app.route('/snapshot', methods=['GET'])
@app.route('/snapshot/', methods=['GET'])
@app.route('/snapshot/<path:path>', methods=['GET'])
def snapshot(path = ''):
parsed = urlparse(request.url)
final_url = '%s://%s/%s' % (parsed.scheme, 'localhost', path)
result = render_snapshot(final_url)
if result:
return result
abort(404)
@app.route('/plans/') @app.route('/plans/')
def plans(): def plans():
return index('') return index('')
@ -82,11 +100,18 @@ def new():
def repository(): def repository():
return index('') return index('')
@app.route('/security/')
def security():
return index('')
@app.route('/v1') @app.route('/v1')
@app.route('/v1/') @app.route('/v1/')
def v1(): def v1():
return index('') return index('')
@app.route('/status', methods=['GET']) @app.route('/status', methods=['GET'])
def status(): def status():
return make_response('Healthy') return make_response('Healthy')
@ -97,11 +122,33 @@ def tos():
return render_template('tos.html') return render_template('tos.html')
@app.route('/disclaimer', methods=['GET'])
def disclaimer():
return render_template('disclaimer.html')
@app.route('/privacy', methods=['GET']) @app.route('/privacy', methods=['GET'])
def privacy(): def privacy():
return render_template('privacy.html') return render_template('privacy.html')
@app.route('/receipt', methods=['GET'])
def receipt():
id = request.args.get('id')
if id:
invoice = stripe.Invoice.retrieve(id)
if invoice:
org = model.get_user_or_org_by_customer_id(invoice.customer)
if org and org.organization:
admin_org = AdministerOrganizationPermission(org.username)
if admin_org.can():
file_data = renderInvoiceToPdf(invoice, org)
return Response(file_data,
mimetype="application/pdf",
headers={"Content-Disposition":
"attachment;filename=receipt.pdf"})
abort(404)
def common_login(db_user): def common_login(db_user):
if login_user(_LoginWrappedDBUser(db_user.username, db_user)): if login_user(_LoginWrappedDBUser(db_user.username, db_user)):
logger.debug('Successfully signed in as: %s' % db_user.username) logger.debug('Successfully signed in as: %s' % db_user.username)

View file

@ -45,7 +45,7 @@ http {
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
if ($args ~ "_escaped_fragment_") { if ($args ~ "_escaped_fragment_") {
rewrite ^ /static/snapshots$uri/index.html; rewrite ^ /snapshot$uri;
} }
location /static/ { location /static/ {

View file

@ -9,7 +9,7 @@ boto
pymysql pymysql
stripe stripe
gunicorn gunicorn
eventlet gevent
mixpanel-py mixpanel-py
beautifulsoup4 beautifulsoup4
marisa-trie marisa-trie
@ -17,3 +17,4 @@ apscheduler
python-daemon python-daemon
paramiko paramiko
python-digitalocean python-digitalocean
xhtml2pdf

View file

@ -5,29 +5,34 @@ Flask-Mail==0.9.0
Flask-Principal==0.4.0 Flask-Principal==0.4.0
Jinja2==2.7.1 Jinja2==2.7.1
MarkupSafe==0.18 MarkupSafe==0.18
Pillow==2.2.1
PyMySQL==0.6.1 PyMySQL==0.6.1
Werkzeug==0.9.4 Werkzeug==0.9.4
argparse==1.2.1 argparse==1.2.1
beautifulsoup4==4.3.2 beautifulsoup4==4.3.2
blinker==1.3 blinker==1.3
boto==2.15.0 boto==2.17.0
distribute==0.6.34 distribute==0.6.34
ecdsa==0.10 ecdsa==0.10
eventlet==0.14.0 gevent==0.13.8
greenlet==0.4.1 greenlet==0.4.1
gunicorn==18.0 gunicorn==18.0
html5lib==1.0b3
itsdangerous==0.23 itsdangerous==0.23
lockfile==0.9.1 lockfile==0.9.1
marisa-trie==0.5.1 marisa-trie==0.5.1
mixpanel-py==3.0.0 mixpanel-py==3.0.0
paramiko==1.12.0 paramiko==1.12.0
peewee==2.1.4 peewee==2.1.5
py-bcrypt==0.4 py-bcrypt==0.4
pyPdf==1.13
pycrypto==2.6.1 pycrypto==2.6.1
python-daemon==1.6 python-daemon==1.6
python-dateutil==2.1 python-dateutil==2.2
python-digitalocean==0.5 python-digitalocean==0.5.1
requests==2.0.0 reportlab==2.7
requests==2.0.1
six==1.4.1 six==1.4.1
stripe==1.9.8 stripe==1.9.8
wsgiref==0.1.2 wsgiref==0.1.2
xhtml2pdf==0.0.5

View file

@ -1,13 +0,0 @@
Follow the instructions to set up a host of the whole project before attempting to run.
to run once:
```
python make_snapshot.py
```
cron line to update every 30 minutes:
```
0,30 * * * * cd /home/ubuntu/quay/seo-snapshots && ../venv/bin/python make_snapshot.py
```

View file

@ -1,60 +0,0 @@
import subprocess
import urllib
import os
import logging
import codecs
from bs4 import BeautifulSoup
from Queue import Queue
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
BASE_URL = 'http://localhost:5000'
OUTPUT_PATH = 'snapshots/'
aware_of = set()
crawl_queue = Queue()
def crawl_url(url):
final_url = BASE_URL + url
to_write = OUTPUT_PATH + url + 'index.html'
logger.info('Snapshotting url: %s -> %s' % (final_url, to_write))
out_html = subprocess.check_output(['phantomjs', '--ignore-ssl-errors=yes',
'phantomjs-runner.js', final_url])
# Remove script tags
soup = BeautifulSoup(out_html)
to_extract = soup.findAll('script')
for item in to_extract:
item.extract()
# Find all links and add them to the crawl queue
for link in soup.findAll('a'):
to_add = link.get('href')
if to_add not in aware_of and to_add.startswith('/'):
logger.info('Adding link to be crawled: %s' % to_add)
crawl_queue.put(to_add)
aware_of.add(to_add)
to_write_dir = os.path.dirname(to_write)
if not os.path.exists(to_write_dir):
os.makedirs(to_write_dir)
with codecs.open(to_write, 'w', 'utf-8') as output_file:
output_file.write(soup.prettify())
# Seed the crawler
crawl_queue.put('/')
aware_of.add('/')
# Crawl
while not crawl_queue.empty():
to_crawl = crawl_queue.get()
crawl_url(to_crawl)

View file

@ -1,46 +0,0 @@
import subprocess
import urllib
import os
import logging
import codecs
from bs4 import BeautifulSoup
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
BASE_URL = 'https://localhost/'
OUTPUT_PATH = '../static/snapshots/'
URLS = [
'',
'guide/',
'plans/',
'repository/',
'signin/',
]
for url in URLS:
final_url = BASE_URL + url
to_write = OUTPUT_PATH + url + 'index.html'
logger.info('Snapshotting url: %s -> %s' % (final_url, to_write))
out_html = subprocess.check_output(['phantomjs', '--ignore-ssl-errors=yes',
'phantomjs-runner.js', final_url])
# Remove script tags
soup = BeautifulSoup(out_html)
to_extract = soup.findAll('script')
for item in to_extract:
item.extract()
to_write_dir = os.path.dirname(to_write)
if not os.path.exists(to_write_dir):
os.makedirs(to_write_dir)
with codecs.open(to_write, 'w', 'utf-8') as output_file:
output_file.write(soup.prettify())

View file

@ -1,23 +0,0 @@
var system = require('system');
var url = system.args[1] || '';
if(url.length > 0) {
var page = require('webpage').create();
page.open(url, function (status) {
if (status == 'success') {
var delay, checker = (function() {
var html = page.evaluate(function () {
var ready = document.getElementsByClassName('ready-indicator')[0];
if(ready.getAttribute('data-status') == 'ready') {
return document.getElementsByTagName('html')[0].outerHTML;
}
});
if(html) {
clearTimeout(delay);
console.log(html);
phantom.exit();
}
});
delay = setInterval(checker, 100);
}
});
}

View file

@ -1,5 +1,35 @@
* { * {
font-family: 'Droid Sans', sans-serif; font-family: 'Droid Sans', sans-serif;
margin: 0;
}
html, body {
height: 100%;
}
.tooltip {
word-break: normal !important;
word-wrap: normal !important;
}
.code-info {
border-bottom: 1px dashed #aaa;
}
.content-container {
padding-bottom: 70px;
}
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -136px;
}
.footer-container, .push {
height: 110px;
overflow: hidden;
} }
.button-hidden { .button-hidden {
@ -512,6 +542,15 @@
padding: 20px; padding: 20px;
} }
.jumbotron .disclaimer-link {
font-size: .3em;
vertical-align: 23px;
}
.jumbotron .disclaimer-link:hover {
text-decoration: none;
}
.landing .popover { .landing .popover {
font-size: 14px; font-size: 14px;
} }
@ -644,57 +683,61 @@ form input.ng-valid.ng-dirty,
} }
} }
.landing-footer { .page-footer {
background-color: white; background-color: white;
background-image: none; background-image: none;
padding: 10px; padding: 10px;
padding-bottom: 40px; padding-bottom: 40px;
margin-top: 10px; margin-top: 52px;
border-top: 1px solid #eee; border-top: 1px solid #eee;
} }
.landing-footer .row { .page-footer .row {
max-width: 100%; max-width: 100%;
} }
.landing-footer .copyright-container { .page-footer .logo-container {
font-size: 11px; font-size: 11px;
color: black; color: black;
text-align: right; text-align: right;
padding-right: 30px; padding-right: 30px;
padding-top: 20px;
} }
.landing-footer .copyright-container a { .page-footer .logo-container a {
display: block; display: block;
margin-top: 4px;
margin-right: 10px;
} }
.landing-footer .dt-logo { .page-footer .dt-logo {
vertical-align: center; vertical-align: center;
max-width: 150px; max-width: 150px;
} }
.landing-footer .copyright { .page-footer .copyright {
font-size: 11px; font-size: 12px;
color: black; color: black;
} }
.landing-footer ul { .page-footer ul {
list-style-type: none; list-style-type: none;
margin: 0px; margin: 0px;
padding-left: 0px; padding-left: 0px;
margin-top: 10px;
} }
.landing-footer li { .page-footer li {
margin: 0px; margin: 0px;
display: inline-block;
padding: 10px;
} }
.user-welcome { .user-welcome {
text-align: center; text-align: center;
} }
#repoSearch { .repo-search-box {
width: 300px; width: 240px;
} }
.repo-mini-listing { .repo-mini-listing {
@ -718,7 +761,6 @@ form input.ng-valid.ng-dirty,
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.entity-mini-listing { .entity-mini-listing {
margin: 2px; margin: 2px;
} }
@ -827,7 +869,13 @@ p.editable:hover i {
.repo .empty-message { .repo .empty-message {
padding: 6px; padding: 6px;
font-size: 1.8em; font-size: 1.8em;
color: #ccc; color: #666;
margin-bottom: 10px;
}
.repo .empty-description {
max-width: 600px;
padding: 6px;
} }
.repo dl.dl-horizontal dt { .repo dl.dl-horizontal dt {
@ -1067,6 +1115,12 @@ p.editable:hover i {
padding-left: 44px; padding-left: 44px;
} }
.repo-admin .right-info {
font-size: 11px;
margin-top: 10px;
text-align: right;
}
.repo-admin .entity-search input { .repo-admin .entity-search input {
width: 300px; width: 300px;
} }

View file

@ -36,4 +36,22 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangecardModal">
<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 change credit card</h4>
</div>
<div class="modal-body">
Your credit card could not be changed
</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 -->
</div> </div>

View file

@ -22,15 +22,17 @@
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous"> <ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
<form class="navbar-form navbar-left" role="search"> <li>
<div class="form-group"> <form class="navbar-form navbar-left" role="search">
<input id="repoSearch" type="text" class="form-control" placeholder="Find Repo"> <div class="form-group">
</div> <span class="repo-search"></span>
</form> </div>
</form>
<span class="navbar-left user-tools" ng-show="!user.anonymous"> <span class="navbar-left user-tools" ng-show="!user.anonymous">
<a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" title="Create new repository"></i></a> <a href="/new/"><i class="fa fa-upload user-tool" bs-tooltip="tooltip.title" data-placement="bottom" title="Create new repository"></i></a>
</span> </span>
</li>
<li class="dropdown" ng-switch-when="false"> <li class="dropdown" ng-switch-when="false">
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown"> <a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">

View file

@ -1 +1 @@
<span class="markdown-view-content" ng-bind-html-unsafe="getMarkedDown(content, firstLineOnly)"></span> <span class="markdown-view-content" ng-bind-html="getMarkedDown(content, firstLineOnly)"></span>

View file

@ -0,0 +1 @@
<input type="text" class="form-control repo-search-box" placeholder="Find Repo">

View file

@ -47,7 +47,7 @@ function getMarkedDown(string) {
} }
// Start the application code itself. // Start the application code itself.
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) { quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) {
$provide.factory('UserService', ['Restangular', function(Restangular) { $provide.factory('UserService', ['Restangular', function(Restangular) {
var userResponse = { var userResponse = {
verified: false, verified: false,
@ -138,6 +138,25 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
} }
}; };
planService.handleCardError = function(resp) {
if (!planService.isCardError(resp)) { return; }
bootbox.dialog({
"message": resp.data.carderror,
"title": "Credit card issue",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
planService.isCardError = function(resp) {
return resp && resp.data && resp.data.carderror;
};
planService.verifyLoaded = function(callback) { planService.verifyLoaded = function(callback) {
if (plans) { if (plans) {
callback(plans); callback(plans);
@ -259,7 +278,10 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
return; return;
} }
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure']); planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
planService.handleCardError(resp);
callbacks['failure'](resp);
});
}); });
}); });
}; };
@ -269,7 +291,10 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
callbacks['opening'](); callbacks['opening']();
} }
var submitted = false;
var submitToken = function(token) { var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
$scope.$apply(function() { $scope.$apply(function() {
if (callbacks['started']) { if (callbacks['started']) {
callbacks['started'](); callbacks['started']();
@ -281,7 +306,10 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card'; var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card';
var changeCardRequest = Restangular.one(url); var changeCardRequest = Restangular.one(url);
changeCardRequest.customPOST(cardInfo).then(callbacks['success'], callbacks['failure']); changeCardRequest.customPOST(cardInfo).then(callbacks['success'], function(resp) {
planService.handleCardError(resp);
callbacks['failure'](resp);
});
}); });
}; };
@ -321,7 +349,11 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
callbacks['opening'](); callbacks['opening']();
} }
var submitted = false;
var submitToken = function(token) { var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
mixpanel.track('plan_subscribe'); mixpanel.track('plan_subscribe');
$scope.$apply(function() { $scope.$apply(function() {
@ -395,26 +427,31 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
// index rule to make sure that deep links directly deep into the app continue to work. // index rule to make sure that deep links directly deep into the app continue to work.
// WARNING WARNING WARNING // WARNING WARNING WARNING
$routeProvider. $routeProvider.
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl, reloadOnSearch: false}). when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}). hideFooter: true, reloadOnSearch: false}).
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
hideFooter: true}).
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}). when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}). when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}). when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
when('/user/', {title: 'Account Settings', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}). templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
when('/guide/', {title: 'Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}). when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}). when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
when('/signin/', {title: 'Sign In', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}). when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io',
when('/new/', {title: 'Create new repository', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}). templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
when('/organizations/', {title: 'Organizations', templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}). templateUrl: '/static/partials/security.html', controller: SecurityCtrl}).
when('/organizations/new/', {title: 'New Organization', templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}). when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
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',
templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}).
when('/organizations/new/', {title: 'New Organization', description: 'Create a new organization on Quay.io',
templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}).
when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}). when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}).
when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl}). when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl}).
when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}). when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).
when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}). when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}). when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
otherwise({redirectTo: '/'}); otherwise({redirectTo: '/'});
}]). }]).
@ -434,12 +471,12 @@ quayApp.directive('markdownView', function () {
'content': '=content', 'content': '=content',
'firstLineOnly': '=firstLineOnly' 'firstLineOnly': '=firstLineOnly'
}, },
controller: function($scope, $element) { controller: function($scope, $element, $sce) {
$scope.getMarkedDown = function(content, firstLineOnly) { $scope.getMarkedDown = function(content, firstLineOnly) {
if (firstLineOnly) { if (firstLineOnly) {
content = getFirstTextLine(content); content = getFirstTextLine(content);
} }
return getMarkedDown(content); return $sce.trustAsHtml(getMarkedDown(content));
}; };
} }
}; };
@ -661,6 +698,103 @@ quayApp.directive('markdownInput', function () {
}); });
quayApp.directive('repoSearch', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-search.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, Restangular) {
var searchToken = 0;
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
++searchToken;
}, true);
var element = $($element[0].childNodes[0]);
element.typeahead({
name: 'repositories',
remote: {
url: '/api/find/repository?query=%QUERY',
replace: function (url, uriEncodedQuery) {
url = url.replace('%QUERY', uriEncodedQuery);
url += '&cb=' + searchToken;
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.repositories.length; ++i) {
var repo = data.repositories[i];
datums.push({
'value': repo.name,
'tokens': [repo.name, repo.namespace],
'repo': repo
});
}
return datums;
}
},
template: function (datum) {
template = '<div class="repo-mini-listing">';
template += '<i class="fa fa-hdd fa-lg"></i>'
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
if (datum.repo.description) {
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
}
template += '</div>'
return template;
}
});
element.on('typeahead:selected', function (e, datum) {
element.typeahead('setQuery', '');
document.location = '/repository/' + datum.repo.namespace + '/' + datum.repo.name;
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('headerBar', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/header-bar.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, Restangular) {
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser;
}, true);
$scope.signout = function() {
var signoutPost = Restangular.one('signout');
signoutPost.customPOST().then(function() {
UserService.load();
$location.path('/');
});
};
$scope.appLinkTarget = function() {
if ($("div[ng-view]").length === 0) {
return "_self";
}
return "";
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('entitySearch', function () { quayApp.directive('entitySearch', function () {
var number = 0; var number = 0;
var directiveDefinitionObject = { var directiveDefinitionObject = {
@ -795,6 +929,7 @@ quayApp.directive('billingOptions', function () {
}); });
$scope.changeCard = function() { $scope.changeCard = function() {
var previousCard = $scope.currentCard;
$scope.changingCard = true; $scope.changingCard = true;
var callbacks = { var callbacks = {
'opened': function() { $scope.changingCard = true; }, 'opened': function() { $scope.changingCard = true; },
@ -804,9 +939,13 @@ quayApp.directive('billingOptions', function () {
$scope.currentCard = resp.card; $scope.currentCard = resp.card;
$scope.changingCard = false; $scope.changingCard = false;
}, },
'failure': function() { 'failure': function(resp) {
$('#couldnotchangecardModal').modal({});
$scope.changingCard = false; $scope.changingCard = false;
$scope.currentCard = previousCard;
if (!PlanService.isCardError(resp)) {
$('#cannotchangecardModal').modal({});
}
} }
}; };
@ -897,7 +1036,9 @@ quayApp.directive('planManager', function () {
'opened': function() { $scope.planChanging = true; }, 'opened': function() { $scope.planChanging = true; },
'closed': function() { $scope.planChanging = false; }, 'closed': function() { $scope.planChanging = false; },
'success': subscribedToPlan, 'success': subscribedToPlan,
'failure': function() { $scope.planChanging = false; } 'failure': function(resp) {
$scope.planChanging = false;
}
}; };
PlanService.changePlan($scope, $scope.organization, planId, callbacks); PlanService.changePlan($scope, $scope.organization, planId, callbacks);
@ -1123,7 +1264,8 @@ quayApp.directive('ngBlur', function() {
}; };
}); });
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', function($location, $rootScope, Restangular, UserService) { quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', '$http',
function($location, $rootScope, Restangular, UserService, $http) {
Restangular.setErrorInterceptor(function(response) { Restangular.setErrorInterceptor(function(response) {
if (response.status == 401) { if (response.status == 401) {
$('#sessionexpiredModal').modal({}); $('#sessionexpiredModal').modal({});
@ -1137,5 +1279,22 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', function($
if (current.$$route.title) { if (current.$$route.title) {
$rootScope.title = current.$$route.title; $rootScope.title = current.$$route.title;
} }
if (current.$$route.description) {
$rootScope.description = current.$$route.description;
} else {
$rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
}
$rootScope.hideFooter = !!current.$$route.hideFooter;
}); });
var initallyChecked = false;
window.__isLoading = function() {
if (!initallyChecked) {
initallyChecked = true;
return true;
}
return $http.pendingRequests.length > 0;
};
}]); }]);

View file

@ -1,5 +1,6 @@
$.fn.clipboardCopy = function() { $.fn.clipboardCopy = function() {
var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' }); var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
clip.on('complete', function() { clip.on('complete', function() {
// Resets the animation. // Resets the animation.
var elem = $('#clipboardCopied')[0]; var elem = $('#clipboardCopied')[0];
@ -14,74 +15,6 @@ $.fn.clipboardCopy = function() {
}); });
}; };
function HeaderCtrl($scope, $location, UserService, Restangular) {
var searchToken = 0;
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser;
++searchToken;
}, true);
$scope.signout = function() {
var signoutPost = Restangular.one('signout');
signoutPost.customPOST().then(function() {
UserService.load();
$location.path('/');
});
};
$scope.appLinkTarget = function() {
if ($("div[ng-view]").length === 0) {
return "_self";
}
return "";
};
$scope.$on('$includeContentLoaded', function() {
// THIS IS BAD, MOVE THIS TO A DIRECTIVE
$('#repoSearch').typeahead({
name: 'repositories',
remote: {
url: '/api/find/repository?query=%QUERY',
replace: function (url, uriEncodedQuery) {
url = url.replace('%QUERY', uriEncodedQuery);
url += '&cb=' + searchToken;
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.repositories.length; ++i) {
var repo = data.repositories[i];
datums.push({
'value': repo.name,
'tokens': [repo.name, repo.namespace],
'repo': repo
});
}
return datums;
}
},
template: function (datum) {
template = '<div class="repo-mini-listing">';
template += '<i class="fa fa-hdd fa-lg"></i>'
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
if (datum.repo.description) {
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
}
template += '</div>'
return template;
},
});
$('#repoSearch').on('typeahead:selected', function (e, datum) {
$('#repoSearch').typeahead('setQuery', '');
document.location = '/repository/' + datum.repo.namespace + '/' + datum.repo.name
});
});
}
function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserService) { function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserService) {
$scope.sendRecovery = function() { $scope.sendRecovery = function() {
var signinPost = Restangular.one('recovery'); var signinPost = Restangular.one('recovery');
@ -93,15 +26,12 @@ function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserSe
$scope.sent = false; $scope.sent = false;
}); });
}; };
$scope.status = 'ready';
}; };
function PlansCtrl($scope, $location, UserService, PlanService) { function PlansCtrl($scope, $location, UserService, PlanService) {
// Load the list of plans. // Load the list of plans.
PlanService.getPlans(function(plans) { PlanService.getPlans(function(plans) {
$scope.plans = plans; $scope.plans = plans;
$scope.status = 'ready';
}); });
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
@ -126,7 +56,9 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
} }
function GuideCtrl($scope) { function GuideCtrl($scope) {
$scope.status = 'ready'; }
function SecurityCtrl($scope) {
} }
function RepoListCtrl($scope, Restangular, UserService) { function RepoListCtrl($scope, Restangular, UserService) {
@ -257,14 +189,18 @@ function LandingCtrl($scope, $timeout, $location, Restangular, UserService, KeyS
}); });
}; };
$scope.status = 'ready';
browserchrome.update(); browserchrome.update();
} }
function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $timeout) { function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $timeout) {
$rootScope.title = 'Loading...'; $rootScope.title = 'Loading...';
$scope.$on('$destroy', function() {
if ($scope.tree) {
$scope.tree.dispose();
}
});
// Watch for changes to the tag parameter. // Watch for changes to the tag parameter.
$scope.$on('$routeUpdate', function(){ $scope.$on('$routeUpdate', function(){
$scope.setTag($location.search().tag, false); $scope.setTag($location.search().tag, false);
@ -307,6 +243,11 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name); var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
repositoryFetch.get().then(function(repo) { repositoryFetch.get().then(function(repo) {
$rootScope.title = namespace + '/' + name; $rootScope.title = namespace + '/' + name;
var kind = repo.is_public ? 'public' : 'private';
$rootScope.description = jQuery(getFirstTextLine(repo.description)).text() ||
'View of a ' + kind + ' docker repository on Quay';
$scope.repo = repo; $scope.repo = repo;
$scope.setTag($routeParams.tag); $scope.setTag($routeParams.tag);
@ -376,7 +317,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
// Dispose of any existing tree. // Dispose of any existing tree.
if ($scope.tree) { if ($scope.tree) {
$scope.tree.dispose(); $scope.tree.dispose();
} }
// Create the new tree. // Create the new tree.
@ -665,6 +606,8 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
var permissionsFetch = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/'); var permissionsFetch = Restangular.one('repository/' + namespace + '/' + name + '/permissions/' + kind + '/');
permissionsFetch.get().then(function(resp) { permissionsFetch.get().then(function(resp) {
$rootScope.title = 'Settings - ' + namespace + '/' + name; $rootScope.title = 'Settings - ' + namespace + '/' + name;
$rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
': Permissions, web hooks and other settings';
$scope.permissions[kind] = resp.permissions; $scope.permissions[kind] = resp.permissions;
checkLoading(); checkLoading();
}, function() { }, function() {
@ -889,6 +832,8 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
}; };
$scope.image = image; $scope.image = image;
$rootScope.title = 'View Image - ' + image.id; $rootScope.title = 'View Image - ' + image.id;
$rootScope.description = 'Viewing docker image ' + image.id + ' under repository ' + namespace + '/' + name +
': Image changes tree and list view';
}, function() { }, function() {
$rootScope.title = 'Unknown Image'; $rootScope.title = 'Unknown Image';
$scope.loading = false; $scope.loading = false;
@ -1067,13 +1012,17 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
$scope.upgradePlan = function() { $scope.upgradePlan = function() {
var callbacks = { var callbacks = {
'started': function() { $scope.planChanging = true; },
'opened': function() { $scope.planChanging = true; }, 'opened': function() { $scope.planChanging = true; },
'closed': function() { $scope.planChanging = false; }, 'closed': function() { $scope.planChanging = false; },
'success': subscribedToPlan, 'success': subscribedToPlan,
'failure': function() { $('#couldnotsubscribeModal').modal(); $scope.planChanging = false; } 'failure': function(resp) {
$('#couldnotsubscribeModal').modal();
$scope.planChanging = false;
}
}; };
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, null, callbacks); PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
}; };
// Watch the namespace on the repo. If it changes, we update the plan and the public/private // Watch the namespace on the repo. If it changes, we update the plan and the public/private
@ -1127,6 +1076,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) {
$scope.loading = false; $scope.loading = false;
$rootScope.title = orgname; $rootScope.title = orgname;
$rootScope.description = 'Viewing organization ' + orgname;
}, function() { }, function() {
$scope.loading = false; $scope.loading = false;
}); });
@ -1278,6 +1228,7 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
if (resp && resp.is_admin) { if (resp && resp.is_admin) {
$scope.organization = resp; $scope.organization = resp;
$rootScope.title = orgname + ' (Admin)'; $rootScope.title = orgname + ' (Admin)';
$rootScope.description = 'Administration page for organization ' + orgname;
} }
$scope.loading = false; $scope.loading = false;
@ -1355,6 +1306,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
$scope.canEditMembers = resp.can_edit; $scope.canEditMembers = resp.can_edit;
$scope.loading = !$scope.organization || !$scope.members; $scope.loading = !$scope.organization || !$scope.members;
$rootScope.title = teamname + ' (' + $scope.orgname + ')'; $rootScope.title = teamname + ' (' + $scope.orgname + ')';
$rootScope.description = 'Team management page for team ' + teamname + ' under organization ' + orgname;
}, function() { }, function() {
$scope.organization = null; $scope.organization = null;
$scope.members = null; $scope.members = null;

File diff suppressed because one or more lines are too long

View file

@ -1,26 +1,38 @@
<div class="container ready-indicator" data-status="{{ status }}"> <div class="container content-container">
<div class="alert alert-warning">Warning: Quay requires docker version 0.6.2 or higher to work</div> <div class="alert alert-warning">Warning: Quay requires docker version 0.6.2 or higher to work</div>
<h2>User Guide</h2> <h2>User Guide</h2>
<div class="user-guide container"> <div class="user-guide container">
<h3>Signing into Quay <span class="label label-default">Setup</span></h3>
<h3>Pulling a repository from Quay</h3>
<div class="container"> <div class="container">
<div class="alert alert-info">Note: <b>Private</b> repositories require you to be <b>logged in</b> or the pull will fail. See below for how to sign into Quay if you have never done so before. </div> To setup your Docker client for pushing to Quay, login with your credentials:
To pull a repository from Quay, run the following command:
<br><br> <br><br>
<pre>docker pull quay.io/<i>username/repo_name</i></pre> <pre>$ sudo docker login quay.io
Login against server at https://quay.io/v1/
Username: myusername
Password: mypassword
Email: my@email.com</pre>
</div> </div>
<br> <br>
<h3>Pushing a repository to Quay <span class="label label-success">Requires Write Access</span></h3> <h3>Pushing a repository to Quay <span class="label label-success">Requires Write Access</span></h3>
<div class="container"> <div class="container">
First, tag the image with your repository name:<br><br> In order to push a repository to Quay, it must be <b>tagged</b> with the <b>quay.io</b> domain and the namespace under which it will live:
<pre>docker tag <i>0u123imageid</i> quay.io/<i>username/repo_name</i></pre> <br><br>
<pre>sudo docker tag <i>0u123imageid</i> quay.io/<i>username/repo_name</i></pre>
<br> <br>
Second, push the repository to Quay:<br><br> Once tagged, the repository can be pushed to Quay:<br><br>
<pre>docker push quay.io/<i>username/repo_name</i></pre> <pre>sudo docker push quay.io/<i>username/repo_name</i></pre>
</div>
<br>
<h3>Pulling a repository from Quay</h3>
<div class="container">
<div class="alert alert-info">Note: <b>Private</b> repositories require you to be <b>logged in</b> or the pull will fail. See above for how to sign into Quay if you have never done so before. </div>
To pull a repository from Quay, run the following command:
<br><br>
<pre>sudo docker pull quay.io/<i>username/repo_name</i></pre>
</div> </div>
<br> <br>
@ -58,15 +70,28 @@
<dt>Email</dt><dd>This value is ignored, any value may be used.</dd> <dt>Email</dt><dd>This value is ignored, any value may be used.</dd>
</dl> </dl>
</ul> </ul>
</div> </div>
<br>
<h3>Signing into Quay <span class="label label-default">Optional</span></h3> <a name="#post-hook"></a>
<h3>Using push web hooks <span class="label label-info">Requires Admin Access</span></h3>
<div class="container"> <div class="container">
If you have never pushed a repository to Quay and wish to pull a <b>private</b> repository, you can sign into Quay by running the following command: A repository can have one or more <b>push web hooks</b> setup, which will be invoked whenever <u>a successful push occurs</u>. Web hooks can be managed from the repository's admin interface.
<br><br> <br><br> A web hook will be invoked
<pre>docker login quay.io</pre> as an HTTP <b>POST</b> to the specified URL, with a JSON body describing the push:<br><br>
<pre>
{
<span class="code-info" title="The number of images pushed" bs-tooltip="tooltip.title">"pushed_image_count"</span>: 2,
<span class="code-info" title="The name of the repository (without its namespace)" bs-tooltip="tooltip.title">"name"</span>: "ubuntu",
<span class="code-info" title="The full name of the repository" bs-tooltip="tooltip.title">"repository"</span>:"devtable/ubuntu",
<span class="code-info" title="The URL at which the repository can be pulled by Docker" bs-tooltip="tooltip.title">"docker_url"</span>: "quay.io/devtable/ubuntu",
<span class="code-info" title="Map of updated tag names to their latest image IDs" bs-tooltip="tooltip.title">"updated_tags"</span>: {
"latest": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc"
},
<span class="code-info" title="The namespace of the repository" bs-tooltip="tooltip.title">"namespace"</span>: "devtable",
<span class="code-info" title="Whether the repository is public or private" bs-tooltip="tooltip.title">"visibility"</span>: "private",
<span class="code-info" title="The Quay URL for the repository" bs-tooltip="tooltip.title">"homepage"</span>: "https://quay.io/repository/devtable/ubuntu"
}
</pre>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,11 +1,11 @@
<div class="jumbotron landing ready-indicator" data-status="{{ status }}"> <div class="jumbotron landing">
<div class="container"> <div class="container">
<div class="row messages"> <div class="row messages">
<div class="col-md-7"> <div class="col-md-7">
<div ng-show="user.anonymous"> <div ng-show="user.anonymous">
<h1>Secure hosting for <b>private</b> docker repositories</h1> <h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
<h3>Use the docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3> <h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
<div class="sellcall"><a href="/plans">Private repository plans starting at $7/mo</a></div> <div class="sellcall"><a href="/plans/">Private repository plans starting at $7/mo</a></div>
</div> </div>
<div ng-show="!user.anonymous"> <div ng-show="!user.anonymous">
@ -72,17 +72,16 @@
<i class="fa fa-lock"></i> <i class="fa fa-lock"></i>
<b>Secure</b> <b>Secure</b>
<span class="shoutout-expand"> <span class="shoutout-expand">
Store your private docker containers where only you and your team Your data is transferred using <strong>SSL at all times</strong> and <strong>encrypted</strong> when at rest. More information available in our <a href="/security/">security guide</a>
can access it, with communication secured by <strong>SSL at all times</strong> </span>
</span>
</div> </div>
<div class="col-md-4 shoutout"> <div class="col-md-4 shoutout">
<i class="fa fa-user"></i> <i class="fa fa-group"></i>
<b>Shareable</b> <b>Shareable</b>
<span class="shoutout-expand"> <span class="shoutout-expand">
Have to share a repository? No problem! Share with anyone you choose Have to share a repository? No problem! Share with anyone you choose
</span> </span>
</div> </div>
<div class="col-md-4 shoutout"> <div class="col-md-4 shoutout">
@ -90,7 +89,7 @@
<b>Cloud Hosted</b> <b>Cloud Hosted</b>
<span class="shoutout-expand"> <span class="shoutout-expand">
Accessible from anywhere, anytime Accessible from anywhere, anytime
</span> </span>
</div> </div>
</div> <!-- row --> </div> <!-- row -->
</div> <!-- container --> </div> <!-- container -->
@ -147,36 +146,3 @@
</div> </div>
</div> </div>
</div> </div>
<nav class="landing-footer">
<div class="row">
<div class="col-md-2 col-md-offset-1">
<h4>About</h4>
<ul>
<li><a href="http://blog.devtable.com/">Blog</a></li>
</ul>
</div>
<div class="col-md-2">
<h4>Legal</h4>
<ul>
<li><a href="/tos" target="_self">Terms of Service</a></li>
<li><a href="/privacy" target="_self">Privacy Policy</a></li>
</ul>
</div>
<div class="col-md-2">
<h4>Support</h4>
<ul>
<li><a href="mailto:support@quay.io">Contact Support</a></li>
<li><a href="/guide/">User Guide</a></li>
</ul>
</div>
<div class="col-md-3 col-md-offset-2 copyright-container">
<a href="https://devtable.com"><img class="dt-logo" src="/static/img/dt-logo.png"></a>
<span class="copyright">&copy;2013 DevTable, LLC</span>
</div>
</div> <!-- row -->
</nav>

View file

@ -55,12 +55,13 @@
<th>Billing Date/Time</th> <th>Billing Date/Time</th>
<th>Amount Due</th> <th>Amount Due</th>
<th>Status</th> <th>Status</th>
<th></th>
</thead> </thead>
<tbody class="invoice" ng-repeat="invoice in invoices"> <tbody class="invoice" ng-repeat="invoice in invoices">
<tr class="invoice-title" ng-click="toggleInvoice(invoice.id)"> <tr class="invoice-title">
<td><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td> <td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
<td><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td> <td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
<td> <td>
<span class="invoice-status"> <span class="invoice-status">
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span> <span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
@ -69,6 +70,11 @@
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span> <span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
</span> </span>
</td> </td>
<td>
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
</a>
</td>
</tr> </tr>
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse"> <tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">

View file

@ -1,4 +1,4 @@
<div class="container org-list"> <div class="container org-list conntent-container">
<div class="loading" ng-show="loading"> <div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i> <i class="fa fa-spinner fa-spin fa-3x"></i>
</div> </div>

View file

@ -1,4 +1,4 @@
<div class="container plans ready-indicator" data-status="{{ status }}"> <div class="container plans content-container">
<div class="callout"> <div class="callout">
Plans &amp; Pricing Plans &amp; Pricing
</div> </div>

View file

@ -1,6 +1,3 @@
<script src="static/lib/Blob.js"></script>
<script src="static/lib/FileSaver.js"></script>
<script src="static/lib/jquery.base64.min.js"></script>
<div class="loading" ng-show="loading"> <div class="loading" ng-show="loading">
<i class="fa fa-spinner fa-spin fa-3x"></i> <i class="fa fa-spinner fa-spin fa-3x"></i>
@ -23,7 +20,7 @@
<div class="col-md-2"> <div class="col-md-2">
<ul class="nav nav-pills nav-stacked"> <ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li> <li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#webhook" ng-click="loadWebhooks()">Webhooks</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#webhook" ng-click="loadWebhooks()">Web Hooks</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete</a></li>
</ul> </ul>
@ -152,19 +149,19 @@
<!-- Webhook tab --> <!-- Webhook tab -->
<div id="webhook" class="tab-pane"> <div id="webhook" class="tab-pane">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Push Webhooks <div class="panel-heading">Push Web Hooks
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="URLs which will be invoked when a successful push to the repository occurs."></i> <i class="info-icon fa fa-info-circle" data-placement="left" data-content="URLs which will be invoked with an HTTP POST and JSON payload when a successful push to the repository occurs."></i>
</div> </div>
<div class="panel-body" ng-show="webhooksLoading"> <div class="panel-body" ng-show="webhooksLoading">
Loading webhooks: <i class="fa fa-spinner fa-spin fa-2x" style="vertical-align: middle; margin-left: 4px"></i> Loading web hooks: <i class="fa fa-spinner fa-spin fa-2x" style="vertical-align: middle; margin-left: 4px"></i>
</div> </div>
<div class="panel-body" ng-show="!webhooksLoading"> <div class="panel-body" ng-show="!webhooksLoading">
<table class="permissions" ng-form="newWebhookForm"> <table class="permissions" ng-form="newWebhookForm">
<thead> <thead>
<tr> <tr>
<td style="width: 500px;">Webhook URL</td> <td style="width: 500px;">Web Hook URL</td>
<td></td> <td></td>
</tr> </tr>
</thead> </thead>
@ -175,13 +172,13 @@
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" tabindex="0">
<span class="delete-ui-button" ng-click="deleteWebhook(webhook)"><button class="btn btn-danger">Delete</button></span> <span class="delete-ui-button" ng-click="deleteWebhook(webhook)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Webhook"></i> <i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Web Hook"></i>
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<input type="url" class="form-control" placeholder="New webhook url..." ng-model="newWebhook.url" required> <input type="url" class="form-control" placeholder="New web hook url..." ng-model="newWebhook.url" required>
</td> </td>
<td> <td>
<button class="btn btn-primary" type="submit" ng-click="createWebhook()">Create</button> <button class="btn btn-primary" type="submit" ng-click="createWebhook()">Create</button>
@ -189,6 +186,10 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="right-info">
Quay will <b>POST</b> to these web hooks whenever a push occurs. See the <a href="/guide">User Guide</a> for more information.
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,7 +2,7 @@
<i class="fa fa-spinner fa-spin fa-3x"></i> <i class="fa fa-spinner fa-spin fa-3x"></i>
</div> </div>
<div class="container ready-indicator" ng-show="!loading" data-status="{{ loading ? '' : 'ready' }}"> <div class="container" ng-show="!loading">
<div class="repo-list" ng-show="!user.anonymous"> <div class="repo-list" ng-show="!user.anonymous">
<div ng-class="user.organizations.length ? 'section-header' : ''"> <div ng-class="user.organizations.length ? 'section-header' : ''">
<div class="button-bar-right"> <div class="button-bar-right">

View file

@ -0,0 +1,44 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Quay.io Security</h1>
<p>We understand that when you upload one of your repositories to Quay.io that you are trusting us with some potentially very sensitive data. On this page we will lay out our security features and practices to help you make an informed decision about whether you can trust us with your data.</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>SSL Everwhere</h3>
<p>We expressly forbid connections to Quay.io using unencrypted HTTP traffic. This helps keep your data and account information safe on the wire. Our SSL traffic is decrypted on our application servers, so your traffic is encrypted even within the datacenter. We use a 4096-bit RSA key, and after the key exchange is complete, traffic is transferred using 256-bit AES, for the maximum encryption strength.</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Encryption</h3>
<p>Our binary data is currently stored in Amazon's <a href="http://aws.amazon.com/s3/">S3</a> service. We use HTTPS when transferring your data internally between our application servers and S3, so your data is never exposed in plain text on any wire. We use their <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html">server side encryption</a> to protect your data while stored at rest in their data centers.</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Passwords</h3>
<p>There have been a number of high profile leaks recently where companies have been storing their customers' passwords in plain text, an unsalted hash, or a <a href="http://en.wikipedia.org/wiki/Salt_(cryptography)">salted hash</a> where every salt is the same. At Quay.io we use the <a href="http://en.wikipedia.org/wiki/Bcrypt">bcrypt</a> algorithm to generate a salted hash from your password, using a unique salt for each password. This method of storage is safe against <a href="http://en.wikipedia.org/wiki/Rainbow_table">rainbow attacks</a> and is obviously superior to plain-text storage. Your credentials are also never written in plain text to our application logs, a leak that is commonly overlooked.</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Access Controls</h3>
<p>Repositories will only ever be shared with people to whom you delegate access. Repositories created from the Docker command line are private by default and repositories must subsequently made public with an explicit action in the Quay.io UI. We have a test suite which is run before every code push which tests all methods which expose private data with all levels of access to ensure nothing is accidentally leaked.</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Firewalls</h3>
<p>Our application servers and database servers are all protected with firewall settings that only allow communication with known hosts and host groups on sensitive ports (e.g. SSH). None of our servers have SSH password authentication enabled, preventing brute force password attacks.</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Data Resilience</h3>
<p>While not related directly to security, many of you are probably worried about whether you can depend on the data you store in Quay.io. All binary data that we store is stored in Amazon S3 at the highest redundancy level, which Amazon claims provides <a href="http://aws.amazon.com/s3/faqs/#How_is_Amazon_S3_designed_to_achieve_99.999999999%_durability">11-nines of durability</a>. Our service metadata (e.g. logins, tags, teams) is stored in a database which is backed up nightly, and backups are preserved for 7 days.</p>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
<div class="container signin-container ready-indicator" data-status="{{ loading ? '' : 'ready' }}"> <div class="container signin-container">
<div class="row"> <div class="row">
<div class="col-sm-6 col-sm-offset-3"> <div class="col-sm-6 col-sm-offset-3">
<div class="panel-group" id="accordion"> <div class="panel-group" id="accordion">

View file

@ -57,7 +57,15 @@
<!-- Empty message --> <!-- Empty message -->
<div class="repo-content" ng-show="!currentTag.image && !repo.is_building"> <div class="repo-content" ng-show="!currentTag.image && !repo.is_building">
<div class="empty-message">(This repository is empty)</div> <div class="empty-message">
This repository is empty
</div>
<div class="empty-description" ng-show="repo.can_write">
To push images to this repository:<br><br>
<pre>sudo docker tag <i>0u123imageidgoeshere</i> quay.io/{{repo.namespace}}/{{repo.name}}
sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
</div>
</div> </div>
<div class="repo-content" ng-show="!currentTag.image && repo.is_building"> <div class="repo-content" ng-show="!currentTag.image && repo.is_building">
@ -67,7 +75,7 @@
<!-- Content view --> <!-- Content view -->
<div class="repo-content" ng-show="currentTag.image"> <div class="repo-content" ng-show="currentTag.image">
<!-- Image History --> <!-- Image History -->
<div id="image-history"> <div id="image-history" style="max-height: 10px;">
<div class="row"> <div class="row">
<!-- Tree View container --> <!-- Tree View container -->
<div class="col-md-8"> <div class="col-md-8">

34
static/sitemap.xml Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://quay.io/</loc>
<changefreq>hourly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://quay.io/plans/</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://quay.io/organizations/</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://quay.io/repository/</loc>
<changefreq>always</changefreq>
</url>
<url>
<loc>https://quay.io/guide/</loc>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://quay.io/tos</loc>
<changefreq>monthly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://quay.io/privacy</loc>
<changefreq>monthly</changefreq>
<priority>0.4</priority>
</url>
</urlset>

View file

@ -10,7 +10,6 @@
{% endblock %} {% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Hosted private docker repositories. Includes full user management and history. Free for public repositories.">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
@ -37,8 +36,11 @@
<script src="//code.jquery.com/jquery.js"></script> <script src="//code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.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.1.5/angular.min.js"></script>
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script> <script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script> <script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
@ -63,7 +65,6 @@
<script src="static/js/controllers.js"></script> <script src="static/js/controllers.js"></script>
<script src="static/js/graphing.js"></script> <script src="static/js/graphing.js"></script>
<!-- start Mixpanel --><script type="text/javascript"> <!-- start Mixpanel --><script type="text/javascript">
var isProd = document.location.hostname === 'quay.io'; var isProd = document.location.hostname === 'quay.io';
@ -73,13 +74,36 @@ b._i.push([a,e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8cc7cc29c869f9", { track_pageview : false, debug: !isProd });</script><!-- end Mixpanel --> mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8cc7cc29c869f9", { track_pageview : false, debug: !isProd });</script><!-- end Mixpanel -->
</head> </head>
<body> <body>
<!-- Nav bar --> <div ng-class="!hideFooter ? 'wrapper' : ''">
<nav class="navbar navbar-default" role="navigation" ng-include="'/static/partials/header.html'" ng-controller='HeaderCtrl' > <nav class="navbar navbar-default header-bar" role="navigation"></nav>
</nav>
{% block body_content %} {% block body_content %}
{% endblock %} {% endblock %}
<div ng-class="!hideFooter ? 'push' : ''"></div>
</div>
<div class="footer-container" ng-show="!hideFooter">
<nav class="page-footer visible-lg visible-md">
<div class="row">
<div class="col-md-7">
<ul>
<li><span class="copyright">&copy;2013 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="/security/">Security</a></li>
<li><b><a href="mailto:support@quay.io">Contact Support</a></b></li>
</ul>
</div>
<div class="col-md-5 logo-container">
<a href="https://devtable.com"><img class="dt-logo" src="/static/img/dt-logo.png"></a>
</div>
</div> <!-- row -->
</nav>
</div>
<!-- begin olark code --> <!-- begin olark code -->
<script data-cfasync="false" type='text/javascript'>/*<![CDATA[*/window.olark||(function(c){var f=window,d=document,l=f.location.protocol=="https:"?"https:":"http:",z=c.name,r="load";var nt=function(){ <script data-cfasync="false" type='text/javascript'>/*<![CDATA[*/window.olark||(function(c){var f=window,d=document,l=f.location.protocol=="https:"?"https:":"http:",z=c.name,r="load";var nt=function(){
@ -101,5 +125,6 @@ mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8
/* custom configuration goes here (www.olark.com/documentation) */ /* custom configuration goes here (www.olark.com/documentation) */
olark.identify('1189-336-10-9918');/*]]>*/</script><noscript><a href="https://www.olark.com/site/1189-336-10-9918/contact" title="Contact us" target="_blank">Questions? Feedback?</a> powered by <a href="http://www.olark.com?welcome" title="Olark live chat software">Olark live chat software</a></noscript> olark.identify('1189-336-10-9918');/*]]>*/</script><noscript><a href="https://www.olark.com/site/1189-336-10-9918/contact" title="Contact us" target="_blank">Questions? Feedback?</a> powered by <a href="http://www.olark.com?welcome" title="Olark live chat software">Olark live chat software</a></noscript>
<!-- end olark code --> <!-- end olark code -->
</body> </body>
</html> </html>

16
templates/disclaimer.html Normal file
View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}
<title>Docker Trademark Disclaimer · Quay.io</title>
{% endblock %}
{% block body_content %}
<div class="container">
<div class="row">
<div class="col-md-12">
Quay.io is in no way affiliated with or sponsored by Docker, Inc. Docker, Docker logo and dotCloud are trademarks or registered trademarks of Docker, Inc. in the United States and/or other countries. Docker, Inc. and other parties may also have trademark rights in other terms used herein.
</div>
</div>
</div>
{% endblock %}

View file

@ -7,6 +7,7 @@
{% block added_meta %} {% block added_meta %}
<base href="/"> <base href="/">
<meta name="description" content="{% raw %}{{ description }}{% endraw %}"></meta>
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" /> <meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
<meta name="fragment" content="!" /> <meta name="fragment" content="!" />
{% endblock %} {% endblock %}
@ -20,17 +21,21 @@
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/zeroclipboard/1.1.7/ZeroClipboard.min.js"></script> <script src="static/lib/ZeroClipboard.min.js"></script>
<script src="static/lib/jquery.overscroll.min.js"></script>
<script src="static/lib/d3-tip.js" charset="utf-8"></script> <script src="static/lib/d3-tip.js" charset="utf-8"></script>
<script src="static/lib/browser-chrome.js"></script> <script src="static/lib/browser-chrome.js"></script>
<script src="static/lib/Blob.js"></script>
<script src="static/lib/FileSaver.js"></script>
<script src="static/lib/jquery.base64.min.js"></script>
{% endblock %} {% endblock %}
{% block body_content %} {% block body_content %}
<div ng-view></div> <!-- Note: Must be in the <body> tag -->
<script src="static/lib/jquery.overscroll.min.js"></script>
<div ng-view></div>
<!-- Modal message dialog --> <!-- Modal message dialog -->
<div class="modal fade" id="sessionexpiredModal" data-backdrop="static"> <div class="modal fade" id="sessionexpiredModal" data-backdrop="static">

View file

@ -4,6 +4,10 @@
<title>Privacy Policy · Quay.io</title> <title>Privacy Policy · Quay.io</title>
{% endblock %} {% endblock %}
{% block added_meta %}
<meta name="description" content="Privacy policy for Quay - Hosted private docker repository">
{% endblock %}
{% block body_content %} {% block body_content %}
<div class="container privacy-policy"> <div class="container privacy-policy">

View file

@ -4,6 +4,10 @@
<title>Terms of Service · Quay.io</title> <title>Terms of Service · Quay.io</title>
{% endblock %} {% endblock %}
{% block added_meta %}
<meta name="description" content="Terms of service for Quay - Hosted private docker repository">
{% endblock %}
{% block body_content %} {% block body_content %}
<div class="tos container"> <div class="tos container">
<h2>Terms of Service</h2> <h2>Terms of Service</h2>

25
tools/freeloaders.py Normal file
View file

@ -0,0 +1,25 @@
from data import model
from data.database import User
from app import stripe
from data.plans import get_plan
def get_private_allowed(customer):
if not customer.stripe_id:
return 0
subscription = stripe.Customer.retrieve(customer.stripe_id).subscription
if subscription is None:
return 0
plan = get_plan(subscription.plan.id)
return plan['privateRepos']
# Find customers who have more private repositories than their plans allow
users = User.select()
usage = [(user.username, model.get_private_repo_count(user.username),
get_private_allowed(user)) for user in users]
for username, used, allowed in usage:
if used > allowed:
print('Violation: %s %s > %s' % (username, used, allowed))

View file

@ -1,5 +1,7 @@
from datetime import datetime from datetime import datetime
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from xhtml2pdf import pisa
import StringIO
jinja_options = { jinja_options = {
"loader": FileSystemLoader('util'), "loader": FileSystemLoader('util'),
@ -7,6 +9,20 @@ jinja_options = {
env = Environment(**jinja_options) env = Environment(**jinja_options)
def renderInvoiceToPdf(invoice, user):
""" Renders a nice PDF display for the given invoice. """
sourceHtml = renderInvoiceToHtml(invoice, user)
output = StringIO.StringIO()
pisaStatus = pisa.CreatePDF(sourceHtml, dest=output)
if pisaStatus.err:
return None
value = output.getvalue()
output.close()
return value
def renderInvoiceToHtml(invoice, user): def renderInvoiceToHtml(invoice, user):
""" Renders a nice HTML display for the given invoice. """ """ Renders a nice HTML display for the given invoice. """
def get_price(price): def get_price(price):

37
util/phantomjs-runner.js Normal file
View file

@ -0,0 +1,37 @@
var system = require('system');
var url = system.args[1] || '';
if(url.length > 0) {
var page = require('webpage').create();
page.open(url, function (status) {
if (status == 'success') {
var delay, checker = (function() {
var html = page.evaluate(function () {
var found = document.getElementsByTagName('html')[0].outerHTML || '';
if (window.__isLoading && !window.__isLoading()) {
return found;
}
if (found.indexOf('404 Not Found') > 0) {
return found;
}
return null;
});
if (html) {
if (html.indexOf('404 Not Found') > 0) {
console.log('Not Found');
phantom.exit();
return;
}
clearTimeout(delay);
console.log(html);
phantom.exit();
}
});
delay = setInterval(checker, 100);
} else {
console.log('Not Found');
phantom.exit();
}
});
}

23
util/seo.py Normal file
View file

@ -0,0 +1,23 @@
import subprocess
import logging
from bs4 import BeautifulSoup
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
def render_snapshot(url):
logger.info('Snapshotting url: %s' % url)
out_html = subprocess.check_output(['phantomjs', '--ignore-ssl-errors=yes',
'util/phantomjs-runner.js', url])
if not out_html or out_html.strip() == 'Not Found':
return None
# Remove script tags
soup = BeautifulSoup(out_html.decode('utf8'))
to_extract = soup.findAll('script')
for item in to_extract:
item.extract()
return str(soup)