Merge remote-tracking branch 'origin/dockerbuild'
Conflicts: static/css/quay.css
This commit is contained in:
commit
65aad1a2d9
52 changed files with 2117 additions and 204 deletions
|
@ -1,6 +1,7 @@
|
||||||
to prepare a new host:
|
to prepare a new host:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
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
|
||||||
|
@ -30,6 +31,7 @@ start the workers:
|
||||||
|
|
||||||
```
|
```
|
||||||
STACK=prod python -m workers.diffsworker -D
|
STACK=prod python -m workers.diffsworker -D
|
||||||
|
STACK=prod python -m workers.dockerfilebuild -D
|
||||||
```
|
```
|
||||||
|
|
||||||
bouncing the servers:
|
bouncing the servers:
|
||||||
|
|
|
@ -2,16 +2,25 @@ import logging
|
||||||
|
|
||||||
from app import app as application
|
from app import app as application
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(**application.config['LOGGING_CONFIG'])
|
||||||
|
|
||||||
|
|
||||||
import endpoints.index
|
import endpoints.index
|
||||||
import endpoints.api
|
import endpoints.api
|
||||||
import endpoints.web
|
import endpoints.web
|
||||||
import endpoints.tags
|
import endpoints.tags
|
||||||
import endpoints.registry
|
import endpoints.registry
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if application.config.get('INCLUDE_TEST_ENDPOINTS', False):
|
||||||
|
logger.debug('Loading test endpoints.')
|
||||||
|
import endpoints.test
|
||||||
|
|
||||||
# Remove this for prod config
|
# Remove this for prod config
|
||||||
application.debug = True
|
application.debug = True
|
||||||
|
|
||||||
logging.basicConfig(**application.config['LOGGING_CONFIG'])
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
application.run(port=5000, debug=True, host='0.0.0.0')
|
application.run(port=5000, debug=True, threaded=True, host='0.0.0.0')
|
||||||
|
|
26
buildserver/Dockerfile
Normal file
26
buildserver/Dockerfile
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
FROM lopter/raring-base
|
||||||
|
MAINTAINER jake@devtable.com
|
||||||
|
|
||||||
|
RUN echo deb http://archive.ubuntu.com/ubuntu precise universe > /etc/apt/sources.list.d/universe.list
|
||||||
|
RUN apt-get update -qq
|
||||||
|
RUN apt-get install -qqy iptables ca-certificates lxc python-virtualenv git python-dev xz-utils aufs-tools
|
||||||
|
|
||||||
|
# This will use the latest public release. To use your own, comment it out...
|
||||||
|
ADD https://get.docker.io/builds/Linux/x86_64/docker-latest /usr/local/bin/docker
|
||||||
|
# ...then uncomment the following line, and copy your docker binary to current dir.
|
||||||
|
#ADD ./docker /usr/local/bin/docker
|
||||||
|
|
||||||
|
# Install the files
|
||||||
|
ADD ./startserver /usr/local/bin/startserver
|
||||||
|
ADD ./buildserver.py ./buildserver.py
|
||||||
|
ADD ./requirements.txt ./requirements.txt
|
||||||
|
|
||||||
|
RUN chmod +x /usr/local/bin/docker /usr/local/bin/startserver
|
||||||
|
|
||||||
|
RUN virtualenv --distribute venv
|
||||||
|
RUN venv/bin/pip install -r requirements.txt
|
||||||
|
|
||||||
|
VOLUME /var/lib/docker
|
||||||
|
|
||||||
|
EXPOSE 5002:5002
|
||||||
|
CMD startserver
|
13
buildserver/Readme.md
Normal file
13
buildserver/Readme.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
To build:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo docker build -t quay.io/quay/buildserver .
|
||||||
|
sudo docker push quay.io/quay/buildserver
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo docker pull quay.io/quay/buildserver
|
||||||
|
sudo docker run -d -privileged -lxc-conf="lxc.aa_profile=unconfined" quay.io/quay/buildserver
|
||||||
|
```
|
212
buildserver/buildserver.py
Normal file
212
buildserver/buildserver.py
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
import docker
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask import Flask, jsonify, url_for, abort, make_response
|
||||||
|
from zipfile import ZipFile
|
||||||
|
from tempfile import TemporaryFile, mkdtemp
|
||||||
|
from uuid import uuid4
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
|
||||||
|
BUFFER_SIZE = 8 * 1024
|
||||||
|
LOG_FORMAT = '%(asctime)-15s - %(levelname)s - %(pathname)s - ' + \
|
||||||
|
'%(funcName)s - %(message)s'
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def count_steps(dockerfile_path):
|
||||||
|
with open(dockerfile_path, 'r') as dockerfileobj:
|
||||||
|
steps = 0
|
||||||
|
for line in dockerfileobj.readlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped and stripped[0] is not '#':
|
||||||
|
steps += 1
|
||||||
|
return steps
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_zip(request_file):
|
||||||
|
build_dir = mkdtemp(prefix='docker-build-')
|
||||||
|
|
||||||
|
# Save the zip file to temp somewhere
|
||||||
|
with TemporaryFile() as zip_file:
|
||||||
|
zip_file.write(request_file.content)
|
||||||
|
to_extract = ZipFile(zip_file)
|
||||||
|
to_extract.extractall(build_dir)
|
||||||
|
|
||||||
|
return build_dir
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_dockerfile(request_file):
|
||||||
|
build_dir = mkdtemp(prefix='docker-build-')
|
||||||
|
dockerfile_path = os.path.join(build_dir, "Dockerfile")
|
||||||
|
with open(dockerfile_path, 'w') as dockerfile:
|
||||||
|
dockerfile.write(request_file.content)
|
||||||
|
|
||||||
|
return build_dir
|
||||||
|
|
||||||
|
|
||||||
|
def build_image(build_dir, tag_name, num_steps, result_object):
|
||||||
|
try:
|
||||||
|
logger.debug('Starting build.')
|
||||||
|
docker_cl = docker.Client(version='1.5')
|
||||||
|
result_object['status'] = 'building'
|
||||||
|
build_status = docker_cl.build(path=build_dir, tag=tag_name)
|
||||||
|
|
||||||
|
current_step = 0
|
||||||
|
built_image = None
|
||||||
|
for status in build_status:
|
||||||
|
step_increment = re.search(r'Step ([0-9]+) :', status)
|
||||||
|
if step_increment:
|
||||||
|
current_step = int(step_increment.group(1))
|
||||||
|
logger.debug('Step now: %s/%s' % (current_step, num_steps))
|
||||||
|
result_object['current_command'] = current_step
|
||||||
|
continue
|
||||||
|
|
||||||
|
complete = re.match(r'Successfully built ([a-z0-9]+)$', status)
|
||||||
|
if complete:
|
||||||
|
built_image = complete.group(1)
|
||||||
|
logger.debug('Final image ID is: %s' % built_image)
|
||||||
|
continue
|
||||||
|
|
||||||
|
shutil.rmtree(build_dir)
|
||||||
|
|
||||||
|
# Get the image count
|
||||||
|
if not built_image:
|
||||||
|
result_object['status'] = 'error'
|
||||||
|
result_object['message'] = 'Unable to build dockerfile.'
|
||||||
|
return
|
||||||
|
|
||||||
|
history = docker_cl.history(built_image)
|
||||||
|
num_images = len(history)
|
||||||
|
result_object['total_images'] = num_images
|
||||||
|
|
||||||
|
result_object['status'] = 'pushing'
|
||||||
|
logger.debug('Pushing to tag name: %s' % tag_name)
|
||||||
|
resp = docker_cl.push(tag_name)
|
||||||
|
|
||||||
|
current_image = 0
|
||||||
|
image_progress = 0
|
||||||
|
for status in resp:
|
||||||
|
if u'status' in status:
|
||||||
|
status_msg = status[u'status']
|
||||||
|
next_image = r'(Pushing|Image) [a-z0-9]+( already pushed, skipping)?$'
|
||||||
|
match = re.match(next_image, status_msg)
|
||||||
|
if match:
|
||||||
|
current_image += 1
|
||||||
|
image_progress = 0
|
||||||
|
logger.debug('Now pushing image %s/%s' %
|
||||||
|
(current_image, num_images))
|
||||||
|
|
||||||
|
elif status_msg == u'Pushing' and u'progress' in status:
|
||||||
|
percent = r'\(([0-9]+)%\)'
|
||||||
|
match = re.search(percent, status[u'progress'])
|
||||||
|
if match:
|
||||||
|
image_progress = int(match.group(1))
|
||||||
|
|
||||||
|
result_object['current_image'] = current_image
|
||||||
|
result_object['image_completion_percent'] = image_progress
|
||||||
|
|
||||||
|
elif u'errorDetail' in status:
|
||||||
|
result_object['status'] = 'error'
|
||||||
|
if u'message' in status[u'errorDetail']:
|
||||||
|
result_object['message'] = status[u'errorDetail'][u'message']
|
||||||
|
return
|
||||||
|
|
||||||
|
result_object['status'] = 'complete'
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception('Exception when processing request.')
|
||||||
|
result_object['status'] = 'error'
|
||||||
|
result_object['message'] = str(e.message)
|
||||||
|
|
||||||
|
|
||||||
|
MIME_PROCESSORS = {
|
||||||
|
'application/zip': prepare_zip,
|
||||||
|
'text/plain': prepare_dockerfile,
|
||||||
|
'application/octet-stream': prepare_dockerfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
build = {
|
||||||
|
'total_commands': None,
|
||||||
|
'total_images': None,
|
||||||
|
'current_command': None,
|
||||||
|
'current_image': None,
|
||||||
|
'image_completion_percent': None,
|
||||||
|
'status': 'waiting',
|
||||||
|
'message': None,
|
||||||
|
}
|
||||||
|
pool = ThreadPool(1)
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_first_request
|
||||||
|
def start_build():
|
||||||
|
resource_url = os.environ['RESOURCE_URL']
|
||||||
|
tag_name = os.environ['TAG']
|
||||||
|
acccess_token = os.environ['TOKEN']
|
||||||
|
|
||||||
|
logger.debug('Starting job with resource url: %s tag: %s and token: %s' %
|
||||||
|
(resource_url, tag_name, acccess_token))
|
||||||
|
|
||||||
|
# Save the token
|
||||||
|
host = re.match(r'([a-z0-9.:]+)/.+/.+$', tag_name)
|
||||||
|
if host:
|
||||||
|
docker_endpoint = 'http://%s/v1/' % host.group(1)
|
||||||
|
dockercfg_path = os.path.join(os.environ.get('HOME', '.'), '.dockercfg')
|
||||||
|
token = b64encode('$token:%s' % acccess_token)
|
||||||
|
with open(dockercfg_path, 'w') as dockercfg:
|
||||||
|
payload = {
|
||||||
|
docker_endpoint: {
|
||||||
|
'auth': token,
|
||||||
|
'email': '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dockercfg.write(json.dumps(payload))
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid tag name: %s' % tag_name)
|
||||||
|
|
||||||
|
docker_resource = requests.get(resource_url)
|
||||||
|
c_type = docker_resource.headers['content-type']
|
||||||
|
|
||||||
|
logger.info('Request to build file of type: %s with tag: %s' %
|
||||||
|
(c_type, tag_name))
|
||||||
|
|
||||||
|
if c_type not in MIME_PROCESSORS:
|
||||||
|
raise Exception('Invalid dockerfile content type: %s' % c_type)
|
||||||
|
|
||||||
|
build_dir = MIME_PROCESSORS[c_type](docker_resource)
|
||||||
|
|
||||||
|
dockerfile_path = os.path.join(build_dir, "Dockerfile")
|
||||||
|
num_steps = count_steps(dockerfile_path)
|
||||||
|
logger.debug('Dockerfile had %s steps' % num_steps)
|
||||||
|
|
||||||
|
logger.info('Sending job to builder pool.')
|
||||||
|
build['total_commands'] = num_steps
|
||||||
|
|
||||||
|
pool.apply_async(build_image, [build_dir, tag_name, num_steps,
|
||||||
|
build])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/build/', methods=['GET'])
|
||||||
|
def get_status():
|
||||||
|
if build:
|
||||||
|
return jsonify(build)
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/status/', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
return make_response('Running')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
|
||||||
|
app.run(host='0.0.0.0', port=5002, threaded=True)
|
5
buildserver/requirements.txt
Normal file
5
buildserver/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mock==1.0.1
|
||||||
|
requests==1.2.3
|
||||||
|
six==1.3.0
|
||||||
|
flask==0.10.1
|
||||||
|
-e git+git://github.com/DevTable/docker-py.git#egg=docker-py
|
48
buildserver/startserver
Normal file
48
buildserver/startserver
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# First, make sure that cgroups are mounted correctly.
|
||||||
|
CGROUP=/sys/fs/cgroup
|
||||||
|
|
||||||
|
[ -d $CGROUP ] ||
|
||||||
|
mkdir $CGROUP
|
||||||
|
|
||||||
|
mountpoint -q $CGROUP ||
|
||||||
|
mount -n -t tmpfs -o uid=0,gid=0,mode=0755 cgroup $CGROUP || {
|
||||||
|
echo "Could not make a tmpfs mount. Did you use -privileged?"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mount the cgroup hierarchies exactly as they are in the parent system.
|
||||||
|
for SUBSYS in $(cut -d: -f2 /proc/1/cgroup)
|
||||||
|
do
|
||||||
|
[ -d $CGROUP/$SUBSYS ] || mkdir $CGROUP/$SUBSYS
|
||||||
|
mountpoint -q $CGROUP/$SUBSYS ||
|
||||||
|
mount -n -t cgroup -o $SUBSYS cgroup $CGROUP/$SUBSYS
|
||||||
|
done
|
||||||
|
|
||||||
|
# Note: as I write those lines, the LXC userland tools cannot setup
|
||||||
|
# a "sub-container" properly if the "devices" cgroup is not in its
|
||||||
|
# own hierarchy. Let's detect this and issue a warning.
|
||||||
|
grep -q :devices: /proc/1/cgroup ||
|
||||||
|
echo "WARNING: the 'devices' cgroup should be in its own hierarchy."
|
||||||
|
grep -qw devices /proc/1/cgroup ||
|
||||||
|
echo "WARNING: it looks like the 'devices' cgroup is not mounted."
|
||||||
|
|
||||||
|
# Now, close extraneous file descriptors.
|
||||||
|
pushd /proc/self/fd
|
||||||
|
for FD in *
|
||||||
|
do
|
||||||
|
case "$FD" in
|
||||||
|
# Keep stdin/stdout/stderr
|
||||||
|
[012])
|
||||||
|
;;
|
||||||
|
# Nuke everything else
|
||||||
|
*)
|
||||||
|
eval exec "$FD>&-"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
popd
|
||||||
|
|
||||||
|
docker -d &
|
||||||
|
exec venv/bin/python buildserver.py
|
27
certs/digital_ocean
Normal file
27
certs/digital_ocean
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAwjlIK0HodmDNrZAmaALtr9RLriRSeeLh76gV8KHmjRweeT7v
|
||||||
|
dmhKeGP1nOAs17caZkcwsW0tiDbCeIv2MisV405sScjPOxFivWpY8tL72sgVuOAl
|
||||||
|
ReZauOGZ4M1ZcSa/YbT7tnFCIayYE9pde4ih5LmYZqKsBsaNq3ErcMnAzqG77D95
|
||||||
|
8swuVwhz/INioBWwe4FjO76/0DqS357hT5yHDWthJD6UUH12VajPKBtXEvGNUtNL
|
||||||
|
vdq+drm9omt2y0seMn47fZXiNIulLv7ojsWKwtRMTsGcjnv6VMZAVAuX11u4cJd+
|
||||||
|
oPTbDl0D+02B7XYcxABqdMZcOc1/7VTUlFFd4wIDAQABAoIBAAs4V+z3z8AW84rV
|
||||||
|
SwKzOJvxvbV/r6wO6VJ4+Vt/XtxEBZanhhnnCHZP//5iDPUhRMsnza5SSlEWKMHi
|
||||||
|
BAT97DPHcgYJLb+Rz4x1ulG80oPfDzIw8LZLCm6nycXs1v/sZx3z4J63iER9vgNX
|
||||||
|
mBLs371g42b6esmhasm+re3EGflV0LeY1IX0MY40pqGndmW8Fly1QH179TrMzVUJ
|
||||||
|
btu3i2JrwWmKk5zO5YGm0SYY5QQGCdjPj6SL+idDniAefEvbjJYz2qOaPOF3wj/7
|
||||||
|
r8dAnmyaP10Q3JojT01Et5ltMfr0oF2/pic9tWYGrgn/aIuoXUXj0SF3Pfgrb/4L
|
||||||
|
Et1kzFECgYEA8Tb/9bYzQgtaQTQfzFU/KnsIKKnrxh73rZwnIxG59WvN0Ws41Byf
|
||||||
|
rv8fEbXWU8Yj0drxRSud9fADr99lZGWFxle8rSW5+qqoUxG8n/fkktzHxyPE/9Mh
|
||||||
|
pZW7un7a5/glKgUpHpjaOCZj9rhdF1AwdUXLSo1sFc7VBsKvKiKJAT0CgYEAziDt
|
||||||
|
A9h5lOgiLGf1xdBq3qmLIlARz7fivAcZ5acSGN5k6MFFxjHNqhcXRusqs7g+hvCN
|
||||||
|
eRupdwfgSdLwrTfvxuY4pCcddfYIZO3uUZYs/glvYRtIxaP2kMBkZTs9KzI02Bjv
|
||||||
|
zT3NPReR/46SqW0zvYTlRFSY7VZ0eRED/5xnjZ8CgYAZdlrSjyceA6DFXUE2CpGe
|
||||||
|
ZFpaIIW45i/y7ZbcBtUAaR7SymS3T0Yz7M5UykMTmMjTMC9jw9Tqzyk0eXp0fJsA
|
||||||
|
cuaByIe3RCh8jFTC9iH0tsWH6eizsI/OsN2eNCHbdsBFjUHn7u6qGrNWqeN5wIc8
|
||||||
|
+d8ZwY/1RV4LVqWy5u5baQKBgHLFvJMWluQFuPl2zU9etBLU3ma1pKU/I11EqvPH
|
||||||
|
afk044UCEKLBml1pzAkt6jH1lcM2798OOvbPCOCyNlaMvdLG36TvLqU+3/+qx7bf
|
||||||
|
4p90i3LLaWK64BBLP9tp9640n13vzJ5AGiY5GI7uSNVTu6p789hvLlOAfwvmII7T
|
||||||
|
/IjLAoGBAO6iU8i6pAOaKa7+/uExXx6xwk3vqQtovxByo1/m7NpyUtT+ElDSq+t9
|
||||||
|
7f+3TzzPB6ggdMl8d+PSyHR3o7KjVPgOSe7zld7eePhUrLjwZ4lh5ohcvhvYfaRL
|
||||||
|
0EgRTaTb+zLtCAvJS/ilNnJoIcxUmD8u5uSXpY7vAleSOiQTJRTh
|
||||||
|
-----END RSA PRIVATE KEY-----
|
1
certs/digital_ocean.pub
Normal file
1
certs/digital_ocean.pub
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCOUgrQeh2YM2tkCZoAu2v1EuuJFJ54uHvqBXwoeaNHB55Pu92aEp4Y/Wc4CzXtxpmRzCxbS2INsJ4i/YyKxXjTmxJyM87EWK9aljy0vvayBW44CVF5lq44ZngzVlxJr9htPu2cUIhrJgT2l17iKHkuZhmoqwGxo2rcStwycDOobvsP3nyzC5XCHP8g2KgFbB7gWM7vr/QOpLfnuFPnIcNa2EkPpRQfXZVqM8oG1cS8Y1S00u92r52ub2ia3bLSx4yfjt9leI0i6Uu/uiOxYrC1ExOwZyOe/pUxkBUC5fXW7hwl36g9NsOXQP7TYHtdhzEAGp0xlw5zX/tVNSUUV3j jake@coreserver
|
30
certs/quay-staging-enc.key
Normal file
30
certs/quay-staging-enc.key
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
Proc-Type: 4,ENCRYPTED
|
||||||
|
DEK-Info: AES-256-CBC,2E92F159A4A60E842528201723AF241C
|
||||||
|
|
||||||
|
fThdbyOk4whle4mTLn3fDt/eV/JqKXxs18yQl6OdS26sxB80MmwIAF6dyZI3AXHC
|
||||||
|
CBS6mmdZtFgqodXs125XXPijJ0m0V5s2yx7EHeqPMEYLnzu21QfsbtRGXtl5EX3m
|
||||||
|
il+F9Y/+Y3B0ZtvMcRsGPjnCkLeQZwMOVc8DsgC92bqgu3sCDuz70W6Na6wvIlWU
|
||||||
|
2/OViNDSI6QUo90bkdf1H/+pCr8Dge9MBBMnU2G7DqsaLR+ehUMj7sZs/MJjUOK5
|
||||||
|
zERfQMEoywqBatQUlP203GWe3hsMAqAKnc5VUc52mpT7uC8afLuSqqu2SpP9W0f2
|
||||||
|
Fu6jv0D9JqFI7tf11sGYyWdMoP7zSxcquRC+NMVfvEvlRfjourZ8LAkdkQioeGQO
|
||||||
|
uLUoXe55kDfcJMEahUNMbmrwA7pqfsfGkKCMnBTCATvjtghTTCL4xVrK7jwXk7Sv
|
||||||
|
q54TQe27sryYCGnCLErGxEwjs0lnIWsZ+ePa4qgF05JBty3psrFHZlubG+CT8sb7
|
||||||
|
uzBwzjsIW4rgaBdxKzBgcYoUDhxGMqsg3DxcXh9EomsK/ka+SlVnSihYpxnuerxG
|
||||||
|
LYgSQhfSRZRTtHUzQcQuvT5sfa1UQIWWWxeUKxuTTcyE3g1DY3/osatchW9+CCW2
|
||||||
|
z10daPvfdq70OiEnPLSDvD1Gs49QQlfsoaq4lhNU7VBqznr/5bdC4iiQssXbUOjM
|
||||||
|
odREtw0d8Ox72/V/z01/QjTiOIg5tk9tGORVFPmLHC/Db3OUkLHsp/ls8E2IkIMU
|
||||||
|
wrbmvA9ABLgfpLlgivTtYjkG+H9BtSbi4jGHjYGR40unHqzY+6EQfoL7hu6zzacB
|
||||||
|
4jOhKKtR1yskK42VI6vrihT3GaA3MslCBNmEUv/4FkbvRkVifDd7hMylHCkjlUSm
|
||||||
|
zpaydx2gVrRYlum/Ipd+yPb95iniVlaaNe9ZLqtpZt15mEgLwWfrTq470NGLKwbg
|
||||||
|
7IyIrN6TjHSy1BQMS7kQVzc8Mgafa5O3pXr3zJS2JCg13FC7DarRZfNlqQZ7ErRA
|
||||||
|
imF5jxMQC8agbairlrGQ2i+ckY2wm8OEKUu3f8+O1Rq6rHy7SLQq1PSoQQetstFz
|
||||||
|
jLDPRFvtymZb0e510nbsLOcQvaWjda2sU367ed99TkVm0J7PJG3J38B7Sz0Q82hh
|
||||||
|
jfoYsu+kyNCcR2eS7F3E/MAr85r5qW8/0oewV0ZNxZLyAqJLFHKaeEaGcW0q/wb3
|
||||||
|
oDcli8kbX0YnMfm4p7YaR++VYtfiQrhzOkzYNmRV4xbVF3eE6f3lSjGXmeqWhM5c
|
||||||
|
o0+I75UvbjZzvfWjekkwKhzuIHRJV6g5ldxuaJ290xQrdbpzO0xmYKbOHkEtIjow
|
||||||
|
pBIaA6vgmOTnREdQQSjcNcD+eZDuzaKGwCa2IH5k5b025IOWS4XyZ2JNmRzgw2d8
|
||||||
|
mF6fO8ZDpcQUMIv1Hbn4Yc7kE1vOPkUaB3pKumaQW16COoi+EtY+umWf2/SJb+Cd
|
||||||
|
PGfSzcagiR5Fs0wKgHCVTMuoKR/xhaSqLUkCgQoXKRNoT5cO/sTiYygDWyeyRnCE
|
||||||
|
CA3NKDsDnaoCAeDU5XS1tmNJSIxlQjlnFHP71otxMKtV/g42vwTkXmJ+6gH5g9TG
|
||||||
|
-----END RSA PRIVATE KEY-----
|
116
certs/quay-staging-unified.cert
Normal file
116
certs/quay-staging-unified.cert
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGTjCCBTagAwIBAgIDDK/RMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ
|
||||||
|
TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0
|
||||||
|
YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg
|
||||||
|
MSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTMxMDI5MTIxNjM2
|
||||||
|
WhcNMTQxMDMwMTUyMDI0WjBlMRkwFwYDVQQNExBXOHJYcjhsNVBEMkpMQ0VRMQsw
|
||||||
|
CQYDVQQGEwJVUzEYMBYGA1UEAxMPc3RhZ2luZy5xdWF5LmlvMSEwHwYJKoZIhvcN
|
||||||
|
AQkBFhJob3N0bWFzdGVyQHF1YXkuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||||
|
ggEKAoIBAQDq45PoBSyAniiglXyt5yI3kbLcwLXRTrWNCv0rxi+57Elxs/ix7RII
|
||||||
|
Ig1iO1FARJP/ipRRHFB8GNuG+eIdJAEaeB39eyjvGvsOcE8hlK1Hu3Hd3PcKAwaV
|
||||||
|
JpVZyTblUYXy55kw9okwNZJpVJPOHxKaOjNYrJynw92VJ21WeeGk+kh0EZKQ4vtp
|
||||||
|
sMMYIJapQk1CYDdreZoA0TEGZixJG8laUfX+S+CJf9KY7qH8LefjK9fr6x7R+qd4
|
||||||
|
Hvj6lwtGV5UEBkGtU2bzTAOSEMOJBcxOfrPovFFLVvtbYCRAIY2Y5PPvV1Wna9sB
|
||||||
|
h52hxRhoJpwU3/g+LXhJ6UEGFOMvfa+hAgMBAAGjggLdMIIC2TAJBgNVHRMEAjAA
|
||||||
|
MAsGA1UdDwQEAwIDqDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQU6rqK
|
||||||
|
WYOkW9Ya0nerdfpJGA5GbxcwHwYDVR0jBBgwFoAU60I00Jiwq5/0G2sI98xkLu8O
|
||||||
|
LEUwIwYDVR0RBBwwGoIPc3RhZ2luZy5xdWF5LmlvggdxdWF5LmlvMIIBVgYDVR0g
|
||||||
|
BIIBTTCCAUkwCAYGZ4EMAQIBMIIBOwYLKwYBBAGBtTcBAgMwggEqMC4GCCsGAQUF
|
||||||
|
BwIBFiJodHRwOi8vd3d3LnN0YXJ0c3NsLmNvbS9wb2xpY3kucGRmMIH3BggrBgEF
|
||||||
|
BQcCAjCB6jAnFiBTdGFydENvbSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTADAgEB
|
||||||
|
GoG+VGhpcyBjZXJ0aWZpY2F0ZSB3YXMgaXNzdWVkIGFjY29yZGluZyB0byB0aGUg
|
||||||
|
Q2xhc3MgMSBWYWxpZGF0aW9uIHJlcXVpcmVtZW50cyBvZiB0aGUgU3RhcnRDb20g
|
||||||
|
Q0EgcG9saWN5LCByZWxpYW5jZSBvbmx5IGZvciB0aGUgaW50ZW5kZWQgcHVycG9z
|
||||||
|
ZSBpbiBjb21wbGlhbmNlIG9mIHRoZSByZWx5aW5nIHBhcnR5IG9ibGlnYXRpb25z
|
||||||
|
LjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8vY3JsLnN0YXJ0c3NsLmNvbS9jcnQx
|
||||||
|
LWNybC5jcmwwgY4GCCsGAQUFBwEBBIGBMH8wOQYIKwYBBQUHMAGGLWh0dHA6Ly9v
|
||||||
|
Y3NwLnN0YXJ0c3NsLmNvbS9zdWIvY2xhc3MxL3NlcnZlci9jYTBCBggrBgEFBQcw
|
||||||
|
AoY2aHR0cDovL2FpYS5zdGFydHNzbC5jb20vY2VydHMvc3ViLmNsYXNzMS5zZXJ2
|
||||||
|
ZXIuY2EuY3J0MCMGA1UdEgQcMBqGGGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tLzAN
|
||||||
|
BgkqhkiG9w0BAQUFAAOCAQEAfhP/++WewlphCojZwXijpFIy+XX1gR0p4kSfxVgA
|
||||||
|
Anl3khFL/xAvhk6pbWjGQM/9FWb/PFDRgj4fvMKGR8F9bMKNfBOrT+SyWDuI1Ax3
|
||||||
|
y0unu0vZjEfUJmMktrr2aN3NI/bBmdVixNntsHRB0yrrl7Zk0TjQM3I1egfygoxa
|
||||||
|
tfARn5QOO/sReYJXlJdwmFMH0dpMT3++p5RhMZPDVeAdUUK/KzSdlPkVrLPJTKEY
|
||||||
|
d+IAIWjZq5CGOjM9052+CDhyAMvdywJQpxuhO/BzmPrt0ZQwuMdTUutPT2ijDGCB
|
||||||
|
J7nCUCVEtF25KJrJxeXY6oLxgXoaqqU1ZGivAS1oCtnocg==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGNDCCBBygAwIBAgIBGDANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW
|
||||||
|
MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg
|
||||||
|
Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh
|
||||||
|
dGlvbiBBdXRob3JpdHkwHhcNMDcxMDI0MjA1NDE3WhcNMTcxMDI0MjA1NDE3WjCB
|
||||||
|
jDELMAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsT
|
||||||
|
IlNlY3VyZSBEaWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxODA2BgNVBAMTL1N0
|
||||||
|
YXJ0Q29tIENsYXNzIDEgUHJpbWFyeSBJbnRlcm1lZGlhdGUgU2VydmVyIENBMIIB
|
||||||
|
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtonGrO8JUngHrJJj0PREGBiE
|
||||||
|
gFYfka7hh/oyULTTRwbw5gdfcA4Q9x3AzhA2NIVaD5Ksg8asWFI/ujjo/OenJOJA
|
||||||
|
pgh2wJJuniptTT9uYSAK21ne0n1jsz5G/vohURjXzTCm7QduO3CHtPn66+6CPAVv
|
||||||
|
kvek3AowHpNz/gfK11+AnSJYUq4G2ouHI2mw5CrY6oPSvfNx23BaKA+vWjhwRRI/
|
||||||
|
ME3NO68X5Q/LoKldSKqxYVDLNM08XMML6BDAjJvwAwNi/rJsPnIO7hxDKslIDlc5
|
||||||
|
xDEhyBDBLIf+VJVSH1I8MRKbf+fAoKVZ1eKPPvDVqOHXcDGpxLPPr21TLwb0pwID
|
||||||
|
AQABo4IBrTCCAakwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
|
||||||
|
VR0OBBYEFOtCNNCYsKuf9BtrCPfMZC7vDixFMB8GA1UdIwQYMBaAFE4L7xqkQFul
|
||||||
|
F2mHMMo0aEPQQa7yMGYGCCsGAQUFBwEBBFowWDAnBggrBgEFBQcwAYYbaHR0cDov
|
||||||
|
L29jc3Auc3RhcnRzc2wuY29tL2NhMC0GCCsGAQUFBzAChiFodHRwOi8vd3d3LnN0
|
||||||
|
YXJ0c3NsLmNvbS9zZnNjYS5jcnQwWwYDVR0fBFQwUjAnoCWgI4YhaHR0cDovL3d3
|
||||||
|
dy5zdGFydHNzbC5jb20vc2ZzY2EuY3JsMCegJaAjhiFodHRwOi8vY3JsLnN0YXJ0
|
||||||
|
c3NsLmNvbS9zZnNjYS5jcmwwgYAGA1UdIAR5MHcwdQYLKwYBBAGBtTcBAgEwZjAu
|
||||||
|
BggrBgEFBQcCARYiaHR0cDovL3d3dy5zdGFydHNzbC5jb20vcG9saWN5LnBkZjA0
|
||||||
|
BggrBgEFBQcCARYoaHR0cDovL3d3dy5zdGFydHNzbC5jb20vaW50ZXJtZWRpYXRl
|
||||||
|
LnBkZjANBgkqhkiG9w0BAQUFAAOCAgEAIQlJPqWIbuALi0jaMU2P91ZXouHTYlfp
|
||||||
|
tVbzhUV1O+VQHwSL5qBaPucAroXQ+/8gA2TLrQLhxpFy+KNN1t7ozD+hiqLjfDen
|
||||||
|
xk+PNdb01m4Ge90h2c9W/8swIkn+iQTzheWq8ecf6HWQTd35RvdCNPdFWAwRDYSw
|
||||||
|
xtpdPvkBnufh2lWVvnQce/xNFE+sflVHfXv0pQ1JHpXo9xLBzP92piVH0PN1Nb6X
|
||||||
|
t1gW66pceG/sUzCv6gRNzKkC4/C2BBL2MLERPZBOVmTX3DxDX3M570uvh+v2/miI
|
||||||
|
RHLq0gfGabDBoYvvF0nXYbFFSF87ICHpW7LM9NfpMfULFWE7epTj69m8f5SuauNi
|
||||||
|
YpaoZHy4h/OZMn6SolK+u/hlz8nyMPyLwcKmltdfieFcNID1j0cHL7SRv7Gifl9L
|
||||||
|
WtBbnySGBVFaaQNlQ0lxxeBvlDRr9hvYqbBMflPrj0jfyjO1SPo2ShpTpjMM0InN
|
||||||
|
SRXNiTE8kMBy12VLUjWKRhFEuT2OKGWmPnmeXAhEKa2wNREuIU640ucQPl2Eg7PD
|
||||||
|
wuTSxv0JS3QJ3fGz0xk+gA2iCxnwOOfFwq/iI9th4p1cbiCJSS4jarJiwUW0n6+L
|
||||||
|
p/EiO/h94pDQehn7Skzj0n1fSoMD7SfWI55rjbRZotnvbIIp3XUZPD9MEI3vu3Un
|
||||||
|
0q6Dp6jOW6c=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW
|
||||||
|
MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg
|
||||||
|
Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh
|
||||||
|
dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9
|
||||||
|
MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi
|
||||||
|
U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh
|
||||||
|
cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA
|
||||||
|
A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk
|
||||||
|
pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf
|
||||||
|
OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C
|
||||||
|
Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT
|
||||||
|
Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi
|
||||||
|
HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM
|
||||||
|
Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w
|
||||||
|
+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+
|
||||||
|
Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3
|
||||||
|
Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B
|
||||||
|
26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID
|
||||||
|
AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE
|
||||||
|
FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j
|
||||||
|
ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js
|
||||||
|
LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM
|
||||||
|
BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0
|
||||||
|
Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy
|
||||||
|
dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh
|
||||||
|
cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh
|
||||||
|
YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg
|
||||||
|
dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp
|
||||||
|
bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ
|
||||||
|
YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT
|
||||||
|
TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ
|
||||||
|
9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8
|
||||||
|
jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW
|
||||||
|
FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz
|
||||||
|
ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1
|
||||||
|
ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L
|
||||||
|
EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu
|
||||||
|
L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq
|
||||||
|
yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC
|
||||||
|
O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V
|
||||||
|
um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh
|
||||||
|
NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14=
|
||||||
|
-----END CERTIFICATE-----
|
36
certs/quay-staging.cert
Normal file
36
certs/quay-staging.cert
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGTjCCBTagAwIBAgIDDK/RMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ
|
||||||
|
TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0
|
||||||
|
YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg
|
||||||
|
MSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTMxMDI5MTIxNjM2
|
||||||
|
WhcNMTQxMDMwMTUyMDI0WjBlMRkwFwYDVQQNExBXOHJYcjhsNVBEMkpMQ0VRMQsw
|
||||||
|
CQYDVQQGEwJVUzEYMBYGA1UEAxMPc3RhZ2luZy5xdWF5LmlvMSEwHwYJKoZIhvcN
|
||||||
|
AQkBFhJob3N0bWFzdGVyQHF1YXkuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||||
|
ggEKAoIBAQDq45PoBSyAniiglXyt5yI3kbLcwLXRTrWNCv0rxi+57Elxs/ix7RII
|
||||||
|
Ig1iO1FARJP/ipRRHFB8GNuG+eIdJAEaeB39eyjvGvsOcE8hlK1Hu3Hd3PcKAwaV
|
||||||
|
JpVZyTblUYXy55kw9okwNZJpVJPOHxKaOjNYrJynw92VJ21WeeGk+kh0EZKQ4vtp
|
||||||
|
sMMYIJapQk1CYDdreZoA0TEGZixJG8laUfX+S+CJf9KY7qH8LefjK9fr6x7R+qd4
|
||||||
|
Hvj6lwtGV5UEBkGtU2bzTAOSEMOJBcxOfrPovFFLVvtbYCRAIY2Y5PPvV1Wna9sB
|
||||||
|
h52hxRhoJpwU3/g+LXhJ6UEGFOMvfa+hAgMBAAGjggLdMIIC2TAJBgNVHRMEAjAA
|
||||||
|
MAsGA1UdDwQEAwIDqDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQU6rqK
|
||||||
|
WYOkW9Ya0nerdfpJGA5GbxcwHwYDVR0jBBgwFoAU60I00Jiwq5/0G2sI98xkLu8O
|
||||||
|
LEUwIwYDVR0RBBwwGoIPc3RhZ2luZy5xdWF5LmlvggdxdWF5LmlvMIIBVgYDVR0g
|
||||||
|
BIIBTTCCAUkwCAYGZ4EMAQIBMIIBOwYLKwYBBAGBtTcBAgMwggEqMC4GCCsGAQUF
|
||||||
|
BwIBFiJodHRwOi8vd3d3LnN0YXJ0c3NsLmNvbS9wb2xpY3kucGRmMIH3BggrBgEF
|
||||||
|
BQcCAjCB6jAnFiBTdGFydENvbSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTADAgEB
|
||||||
|
GoG+VGhpcyBjZXJ0aWZpY2F0ZSB3YXMgaXNzdWVkIGFjY29yZGluZyB0byB0aGUg
|
||||||
|
Q2xhc3MgMSBWYWxpZGF0aW9uIHJlcXVpcmVtZW50cyBvZiB0aGUgU3RhcnRDb20g
|
||||||
|
Q0EgcG9saWN5LCByZWxpYW5jZSBvbmx5IGZvciB0aGUgaW50ZW5kZWQgcHVycG9z
|
||||||
|
ZSBpbiBjb21wbGlhbmNlIG9mIHRoZSByZWx5aW5nIHBhcnR5IG9ibGlnYXRpb25z
|
||||||
|
LjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8vY3JsLnN0YXJ0c3NsLmNvbS9jcnQx
|
||||||
|
LWNybC5jcmwwgY4GCCsGAQUFBwEBBIGBMH8wOQYIKwYBBQUHMAGGLWh0dHA6Ly9v
|
||||||
|
Y3NwLnN0YXJ0c3NsLmNvbS9zdWIvY2xhc3MxL3NlcnZlci9jYTBCBggrBgEFBQcw
|
||||||
|
AoY2aHR0cDovL2FpYS5zdGFydHNzbC5jb20vY2VydHMvc3ViLmNsYXNzMS5zZXJ2
|
||||||
|
ZXIuY2EuY3J0MCMGA1UdEgQcMBqGGGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tLzAN
|
||||||
|
BgkqhkiG9w0BAQUFAAOCAQEAfhP/++WewlphCojZwXijpFIy+XX1gR0p4kSfxVgA
|
||||||
|
Anl3khFL/xAvhk6pbWjGQM/9FWb/PFDRgj4fvMKGR8F9bMKNfBOrT+SyWDuI1Ax3
|
||||||
|
y0unu0vZjEfUJmMktrr2aN3NI/bBmdVixNntsHRB0yrrl7Zk0TjQM3I1egfygoxa
|
||||||
|
tfARn5QOO/sReYJXlJdwmFMH0dpMT3++p5RhMZPDVeAdUUK/KzSdlPkVrLPJTKEY
|
||||||
|
d+IAIWjZq5CGOjM9052+CDhyAMvdywJQpxuhO/BzmPrt0ZQwuMdTUutPT2ijDGCB
|
||||||
|
J7nCUCVEtF25KJrJxeXY6oLxgXoaqqU1ZGivAS1oCtnocg==
|
||||||
|
-----END CERTIFICATE-----
|
27
certs/quay-staging.key
Normal file
27
certs/quay-staging.key
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEA6uOT6AUsgJ4ooJV8reciN5Gy3MC10U61jQr9K8YvuexJcbP4
|
||||||
|
se0SCCINYjtRQEST/4qUURxQfBjbhvniHSQBGngd/Xso7xr7DnBPIZStR7tx3dz3
|
||||||
|
CgMGlSaVWck25VGF8ueZMPaJMDWSaVSTzh8SmjozWKycp8PdlSdtVnnhpPpIdBGS
|
||||||
|
kOL7abDDGCCWqUJNQmA3a3maANExBmYsSRvJWlH1/kvgiX/SmO6h/C3n4yvX6+se
|
||||||
|
0fqneB74+pcLRleVBAZBrVNm80wDkhDDiQXMTn6z6LxRS1b7W2AkQCGNmOTz71dV
|
||||||
|
p2vbAYedocUYaCacFN/4Pi14SelBBhTjL32voQIDAQABAoIBAGW2VIblLqcnVZps
|
||||||
|
AQhhDQ0ZF2XGQTU4qx8/QfAhqusMqaUF9Mw/R06kSD1gSEfXKms+vAj/hM6oCO/C
|
||||||
|
5yoNPDkVCI+KNGiNu2c+NNXqxrpILf+Pvp3kP4Z4pbWyjwXwLlvH9Csiprdsi1D3
|
||||||
|
IeXgyLJmP3PHkzKGez4qS4tlzdMdBbJkdCQiE35yyF2os7F4HbehQ2Qyfw8PZk9o
|
||||||
|
T8uUEyh7SjYqmxJ2GfGXQd7+NXb3S1j7ehk/XTzlxgkhMW+eWk4hRAhd4j7FICAD
|
||||||
|
0UYx9K/j2TP8tNHgNd0k/BZkIbic6FD09YagPRu71Tc7MPvcSPm9SMDOj3WXJNC9
|
||||||
|
/oDsOEECgYEA+w6mPpPxlspaEPsCYNo1/FdnmOYecLhruzqUU6lbSBhzW6p0R7H2
|
||||||
|
GmfLeE4mGvkPJbx/zU13sRRhtzRB7QjuzZhKgrO8c/UoJeSaFimI3NYwjhtvszU6
|
||||||
|
ActNQOpq5WvBXEOi+FegNW5+6vTnmxGG+gj8nSsu0JBPc2Db6jeFQbkCgYEA74Nw
|
||||||
|
X/iqRtuz+yabc4ALyowHJHdI5FUHv8uPv9Fk9KVRvxwq8Ak0ZcWnO0Sc+0eUC/v+
|
||||||
|
VDVSvf1O+pMli+zIoAzGmLQFt/Is3E4frbBI7D3tWFjCzyduyVNbQBwhzfCDjk7z
|
||||||
|
Xr/vQ1tLlll5QhABtUdJlWIvZFRfm1Qi/un/8SkCgYAaui+Gn/drRzWZcy+IohJ3
|
||||||
|
P9LemzkIZQnLD+x0j6YRIdE+JAJnE5IQs5Ycw60Y2AT9zniIocOpTXMtrtmJ45aQ
|
||||||
|
urLMAViBu8q/ZfvlehyA7iiTKGaW3IbFZCBgVdR1gig+q1CxQZrjtVS7rMDvaElH
|
||||||
|
WyeRj+RW/dYHgXtIDwsXuQKBgQDNYCOH5636vIGEJgK981opABFPz4kNYWwXpfFJ
|
||||||
|
RcAPl4KVIQ4gbYQkkGtpgtgpD6N+80GN63tbtk12x542cX9G3i2c2yDcLikRb1vy
|
||||||
|
j2q4SBGw48uH3gQ9VeC1BGpoMIheCozc/i+nzizuayJy507PpqUOFvcUTNT+WBL6
|
||||||
|
CqSQoQKBgQCAtGhhTn35n/MlNKSrJ6mo8uKmkjX04trlusE+Ub9J2Bb+MT+Vdy/h
|
||||||
|
vsxAJWH9xcTPr6fTGmsiHBP2q8HzU36CFJPsk8mLnnT2Q5rd4nc3KMmXGu/NUwxA
|
||||||
|
AzNbRNJ45O+BZfm6YfZUXhHq4YmNauNwoWzAWmdP1701xI/1Jtmfeg==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
29
config.py
29
config.py
|
@ -42,10 +42,13 @@ class RDSMySQL(object):
|
||||||
DB_DRIVER = MySQLDatabase
|
DB_DRIVER = MySQLDatabase
|
||||||
|
|
||||||
|
|
||||||
class S3Storage(object):
|
class AWSCredentials(object):
|
||||||
AWS_ACCESS_KEY = 'AKIAJWZWUIS24TWSMWRA'
|
AWS_ACCESS_KEY = 'AKIAJWZWUIS24TWSMWRA'
|
||||||
AWS_SECRET_KEY = 'EllGwP+noVvzmsUGQJO1qOMk3vm10Vg+UE6xmmpw'
|
AWS_SECRET_KEY = 'EllGwP+noVvzmsUGQJO1qOMk3vm10Vg+UE6xmmpw'
|
||||||
REGISTRY_S3_BUCKET = 'quay-registry'
|
REGISTRY_S3_BUCKET = 'quay-registry'
|
||||||
|
|
||||||
|
|
||||||
|
class S3Storage(AWSCredentials):
|
||||||
STORAGE_KIND = 's3'
|
STORAGE_KIND = 's3'
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,21 +88,34 @@ class GitHubProdConfig(GitHubTestConfig):
|
||||||
GITHUB_CLIENT_SECRET = 'f89d8bb28ea3bd4e1c68808500d185a816be53b1'
|
GITHUB_CLIENT_SECRET = 'f89d8bb28ea3bd4e1c68808500d185a816be53b1'
|
||||||
|
|
||||||
|
|
||||||
|
class DigitalOceanConfig(object):
|
||||||
|
DO_CLIENT_ID = 'LJ44y2wwYj1MD0BRxS6qHA'
|
||||||
|
DO_CLIENT_SECRET = 'b9357a6f6ff45a33bb03f6dbbad135f9'
|
||||||
|
DO_SSH_KEY_ID = '46986'
|
||||||
|
DO_SSH_PRIVATE_KEY_FILENAME = 'certs/digital_ocean'
|
||||||
|
DO_ALLOWED_REGIONS = {1, 4}
|
||||||
|
|
||||||
|
|
||||||
|
class BuildNodeConfig(object):
|
||||||
|
BUILD_NODE_PULL_TOKEN = 'F02O2E86CQLKZUQ0O81J8XDHQ6F0N1V36L9JTOEEK6GKKMT1GI8PTJQT4OU88Y6G'
|
||||||
|
|
||||||
|
|
||||||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
||||||
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig):
|
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig,
|
||||||
REGISTRY_SERVER = 'localhost:5000'
|
DigitalOceanConfig, AWSCredentials, BuildNodeConfig):
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
'level': logging.DEBUG,
|
'level': logging.DEBUG,
|
||||||
'format': LOG_FORMAT
|
'format': LOG_FORMAT
|
||||||
}
|
}
|
||||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||||
POPULATE_DB_TEST_DATA = True
|
POPULATE_DB_TEST_DATA = True
|
||||||
|
INCLUDE_TEST_ENDPOINTS = True
|
||||||
|
|
||||||
|
|
||||||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||||
StripeLiveConfig, MixpanelTestConfig,
|
StripeLiveConfig, MixpanelTestConfig,
|
||||||
GitHubProdConfig):
|
GitHubProdConfig, DigitalOceanConfig,
|
||||||
REGISTRY_SERVER = 'localhost:5000'
|
BuildNodeConfig):
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
'level': logging.DEBUG,
|
'level': logging.DEBUG,
|
||||||
'format': LOG_FORMAT
|
'format': LOG_FORMAT
|
||||||
|
@ -109,8 +125,7 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||||
|
|
||||||
class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||||
StripeLiveConfig, MixpanelProdConfig,
|
StripeLiveConfig, MixpanelProdConfig,
|
||||||
GitHubProdConfig):
|
GitHubProdConfig, DigitalOceanConfig, BuildNodeConfig):
|
||||||
REGISTRY_SERVER = 'quay.io'
|
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
'stream': sys.stderr,
|
'stream': sys.stderr,
|
||||||
'level': logging.DEBUG,
|
'level': logging.DEBUG,
|
||||||
|
|
|
@ -150,6 +150,16 @@ class RepositoryTag(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryBuild(BaseModel):
|
||||||
|
repository = ForeignKeyField(Repository)
|
||||||
|
access_token = ForeignKeyField(AccessToken)
|
||||||
|
resource_key = CharField()
|
||||||
|
tag = CharField()
|
||||||
|
build_node_id = IntegerField(null=True)
|
||||||
|
phase = CharField(default='waiting')
|
||||||
|
status_url = CharField(null=True)
|
||||||
|
|
||||||
|
|
||||||
class QueueItem(BaseModel):
|
class QueueItem(BaseModel):
|
||||||
queue_name = CharField(index=True)
|
queue_name = CharField(index=True)
|
||||||
body = TextField()
|
body = TextField()
|
||||||
|
@ -162,7 +172,7 @@ def initialize_db():
|
||||||
create_model_tables([User, Repository, Image, AccessToken, Role,
|
create_model_tables([User, Repository, Image, AccessToken, Role,
|
||||||
RepositoryPermission, Visibility, RepositoryTag,
|
RepositoryPermission, Visibility, RepositoryTag,
|
||||||
EmailConfirmation, FederatedLogin, LoginService,
|
EmailConfirmation, FederatedLogin, LoginService,
|
||||||
QueueItem])
|
QueueItem, RepositoryBuild])
|
||||||
Role.create(name='admin')
|
Role.create(name='admin')
|
||||||
Role.create(name='write')
|
Role.create(name='write')
|
||||||
Role.create(name='read')
|
Role.create(name='read')
|
||||||
|
|
|
@ -30,6 +30,10 @@ class InvalidTokenException(DataModelException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRepositoryBuildException(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def create_user(username, password, email):
|
def create_user(username, password, email):
|
||||||
if not validate_email(email):
|
if not validate_email(email):
|
||||||
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
||||||
|
@ -283,8 +287,8 @@ def set_repository_visibility(repo, visibility):
|
||||||
repo.save()
|
repo.save()
|
||||||
|
|
||||||
|
|
||||||
def create_repository(namespace, name, owner):
|
def create_repository(namespace, name, owner, visibility='private'):
|
||||||
private = Visibility.get(name='private')
|
private = Visibility.get(name=visibility)
|
||||||
repo = Repository.create(namespace=namespace, name=name,
|
repo = Repository.create(namespace=namespace, name=name,
|
||||||
visibility=private)
|
visibility=private)
|
||||||
admin = Role.get(name='admin')
|
admin = Role.get(name='admin')
|
||||||
|
@ -548,3 +552,28 @@ def load_token_data(code):
|
||||||
return fetched[0]
|
return fetched[0]
|
||||||
else:
|
else:
|
||||||
raise InvalidTokenException('Invalid delegate token code: %s' % code)
|
raise InvalidTokenException('Invalid delegate token code: %s' % code)
|
||||||
|
|
||||||
|
|
||||||
|
def get_repository_build(request_dbid):
|
||||||
|
try:
|
||||||
|
return RepositoryBuild.get(RepositoryBuild.id == request_dbid)
|
||||||
|
except RepositoryBuild.DoesNotExist:
|
||||||
|
msg = 'Unable to locate a build by id: %s' % request_dbid
|
||||||
|
raise InvalidRepositoryBuildException(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def list_repository_builds(namespace_name, repository_name,
|
||||||
|
include_inactive=True):
|
||||||
|
joined = RepositoryBuild.select().join(Repository)
|
||||||
|
filtered = joined
|
||||||
|
if not include_inactive:
|
||||||
|
filtered = filtered.where(RepositoryBuild.phase != 'error',
|
||||||
|
RepositoryBuild.phase != 'complete')
|
||||||
|
fetched = list(filtered.where(Repository.name == repository_name,
|
||||||
|
Repository.namespace == namespace_name))
|
||||||
|
return fetched
|
||||||
|
|
||||||
|
|
||||||
|
def create_repository_build(repo, access_token, resource_key, tag):
|
||||||
|
return RepositoryBuild.create(repository=repo, access_token=access_token,
|
||||||
|
resource_key=resource_key, tag=tag)
|
||||||
|
|
|
@ -54,5 +54,10 @@ class WorkQueue(object):
|
||||||
def complete(self, completed_item):
|
def complete(self, completed_item):
|
||||||
completed_item.delete_instance()
|
completed_item.delete_instance()
|
||||||
|
|
||||||
|
def incomplete(self, incomplete_item):
|
||||||
|
incomplete_item.available = True
|
||||||
|
incomplete_item.save()
|
||||||
|
|
||||||
|
|
||||||
image_diff_queue = WorkQueue('imagediff')
|
image_diff_queue = WorkQueue('imagediff')
|
||||||
|
dockerfile_build_queue = WorkQueue('dockerfilebuild')
|
||||||
|
|
56
data/userfiles.py
Normal file
56
data/userfiles.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import boto
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from boto.s3.key import Key
|
||||||
|
from uuid import uuid4
|
||||||
|
import hmac
|
||||||
|
import time
|
||||||
|
import urllib
|
||||||
|
import base64
|
||||||
|
import sha
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class S3FileWriteException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserRequestFiles(object):
|
||||||
|
def __init__(self, s3_access_key, s3_secret_key, bucket_name):
|
||||||
|
self._s3_conn = boto.connect_s3(s3_access_key, s3_secret_key,
|
||||||
|
is_secure=False)
|
||||||
|
self._bucket_name = bucket_name
|
||||||
|
self._bucket = self._s3_conn.get_bucket(bucket_name)
|
||||||
|
self._access_key = s3_access_key
|
||||||
|
self._secret_key = s3_secret_key
|
||||||
|
self._prefix = 'userfiles'
|
||||||
|
|
||||||
|
def prepare_for_drop(self, mime_type):
|
||||||
|
""" Returns a signed URL to upload a file to our bucket. """
|
||||||
|
logger.debug('Requested upload url with content type: %s' % mime_type)
|
||||||
|
file_id = str(uuid4())
|
||||||
|
full_key = os.path.join(self._prefix, file_id)
|
||||||
|
k = Key(self._bucket, full_key)
|
||||||
|
url = k.generate_url(300, 'PUT', headers={'Content-Type': mime_type})
|
||||||
|
return (url, file_id)
|
||||||
|
|
||||||
|
def store_file(self, flask_file):
|
||||||
|
file_id = str(uuid4())
|
||||||
|
full_key = os.path.join(self._prefix, file_id)
|
||||||
|
k = Key(self._bucket, full_key)
|
||||||
|
logger.debug('Setting s3 content type to: %s' % flask_file.content_type)
|
||||||
|
k.set_metadata('Content-Type', flask_file.content_type)
|
||||||
|
bytes_written = k.set_contents_from_file(flask_file)
|
||||||
|
|
||||||
|
if bytes_written == 0:
|
||||||
|
raise S3FileWriteException('Unable to write file to S3')
|
||||||
|
|
||||||
|
return file_id
|
||||||
|
|
||||||
|
def get_file_url(self, file_id, expires_in=300):
|
||||||
|
full_key = os.path.join(self._prefix, file_id)
|
||||||
|
k = Key(self._bucket, full_key)
|
||||||
|
return k.generate_url(expires_in)
|
105
endpoints/api.py
105
endpoints/api.py
|
@ -1,8 +1,11 @@
|
||||||
import logging
|
import logging
|
||||||
import stripe
|
import stripe
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
|
import urlparse
|
||||||
|
import json
|
||||||
|
|
||||||
from flask import request, make_response, jsonify, abort
|
from flask import request, make_response, jsonify, abort, url_for
|
||||||
from flask.ext.login import login_required, current_user, logout_user
|
from flask.ext.login import login_required, current_user, logout_user
|
||||||
from flask.ext.principal import identity_changed, AnonymousIdentity
|
from flask.ext.principal import identity_changed, AnonymousIdentity
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
@ -11,6 +14,8 @@ from collections import defaultdict
|
||||||
import storage
|
import storage
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
|
from data.userfiles import UserRequestFiles
|
||||||
|
from data.queue import dockerfile_build_queue
|
||||||
from app import app
|
from app import app
|
||||||
from util.email import send_confirmation_email, send_recovery_email
|
from util.email import send_confirmation_email, send_recovery_email
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
@ -170,10 +175,29 @@ def get_matching_users(prefix):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/', methods=['POST'])
|
user_files = UserRequestFiles(app.config['AWS_ACCESS_KEY'],
|
||||||
|
app.config['AWS_SECRET_KEY'],
|
||||||
|
app.config['REGISTRY_S3_BUCKET'])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def create_repo_api():
|
def create_repo_api():
|
||||||
pass
|
owner = current_user.db_user()
|
||||||
|
|
||||||
|
namespace_name = owner.username
|
||||||
|
repository_name = request.get_json()['repository']
|
||||||
|
visibility = request.get_json()['visibility']
|
||||||
|
|
||||||
|
repo = model.create_repository(namespace_name, repository_name, owner,
|
||||||
|
visibility)
|
||||||
|
repo.description = request.get_json()['description']
|
||||||
|
repo.save()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'namespace': namespace_name,
|
||||||
|
'name': repository_name
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/find/repository', methods=['GET'])
|
@app.route('/api/find/repository', methods=['GET'])
|
||||||
|
@ -323,6 +347,9 @@ def get_repo_api(namespace, repository):
|
||||||
tag_dict = {tag.name: tag_view(tag) for tag in tags}
|
tag_dict = {tag.name: tag_view(tag) for tag in tags}
|
||||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||||
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
||||||
|
active_builds = model.list_repository_builds(namespace, repository,
|
||||||
|
include_inactive=False)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'namespace': namespace,
|
'namespace': namespace,
|
||||||
'name': repository,
|
'name': repository,
|
||||||
|
@ -330,13 +357,83 @@ def get_repo_api(namespace, repository):
|
||||||
'tags': tag_dict,
|
'tags': tag_dict,
|
||||||
'can_write': can_write,
|
'can_write': can_write,
|
||||||
'can_admin': can_admin,
|
'can_admin': can_admin,
|
||||||
'is_public': is_public
|
'is_public': is_public,
|
||||||
|
'is_building': len(active_builds) > 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
abort(404) # Not fount
|
abort(404) # Not fount
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/<path:repository>/build/', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
@parse_repository_name
|
||||||
|
def get_repo_builds(namespace, repository):
|
||||||
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
if permission.can():
|
||||||
|
def build_view(build_obj):
|
||||||
|
if build_obj.status_url:
|
||||||
|
# Delegate the status to the build node
|
||||||
|
node_status = requests.get(build_obj.status_url).json()
|
||||||
|
node_status['id'] = build_obj.id
|
||||||
|
return node_status
|
||||||
|
|
||||||
|
# If there was no status url, do the best we can
|
||||||
|
return {
|
||||||
|
'id': build_obj.id,
|
||||||
|
'total_commands': None,
|
||||||
|
'total_images': None,
|
||||||
|
'current_command': None,
|
||||||
|
'current_image': None,
|
||||||
|
'image_completion_percent': None,
|
||||||
|
'status': build_obj.phase,
|
||||||
|
'message': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
builds = model.list_repository_builds(namespace, repository)
|
||||||
|
return jsonify({
|
||||||
|
'builds': [build_view(build) for build in builds]
|
||||||
|
})
|
||||||
|
|
||||||
|
abort(403) # Permissions denied
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/filedrop/', methods=['POST'])
|
||||||
|
def get_filedrop_url():
|
||||||
|
mime_type = request.get_json()['mimeType']
|
||||||
|
(url, file_id) = user_files.prepare_for_drop(mime_type)
|
||||||
|
return jsonify({
|
||||||
|
'url': url,
|
||||||
|
'file_id': file_id
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/<path:repository>/build/', methods=['POST'])
|
||||||
|
@api_login_required
|
||||||
|
@parse_repository_name
|
||||||
|
def request_repo_build(namespace, repository):
|
||||||
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
if permission.can():
|
||||||
|
logger.debug('User requested repository initialization.')
|
||||||
|
dockerfile_id = request.get_json()['file_id']
|
||||||
|
|
||||||
|
repo = model.get_repository(namespace, repository)
|
||||||
|
token = model.create_access_token(repo, 'write')
|
||||||
|
|
||||||
|
host = urlparse.urlparse(request.url).netloc
|
||||||
|
tag = '%s/%s/%s' % (host, repo.namespace, repo.name)
|
||||||
|
build_request = model.create_repository_build(repo, token, dockerfile_id,
|
||||||
|
tag)
|
||||||
|
dockerfile_build_queue.put(json.dumps({'build_id': build_request.id}))
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'started': True
|
||||||
|
})
|
||||||
|
|
||||||
|
abort(403) # Permissions denied
|
||||||
|
|
||||||
|
|
||||||
def role_view(repo_perm_obj):
|
def role_view(repo_perm_obj):
|
||||||
return {
|
return {
|
||||||
'role': repo_perm_obj.role.name
|
'role': repo_perm_obj.role.name
|
||||||
|
|
|
@ -2,6 +2,7 @@ import json
|
||||||
import urllib
|
import urllib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import urlparse
|
||||||
|
|
||||||
from flask import request, make_response, jsonify, abort
|
from flask import request, make_response, jsonify, abort
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
@ -25,7 +26,9 @@ def generate_headers(role='read'):
|
||||||
def wrapper(namespace, repository, *args, **kwargs):
|
def wrapper(namespace, repository, *args, **kwargs):
|
||||||
response = f(namespace, repository, *args, **kwargs)
|
response = f(namespace, repository, *args, **kwargs)
|
||||||
|
|
||||||
response.headers['X-Docker-Endpoints'] = app.config['REGISTRY_SERVER']
|
# We run our index and registry on the same hosts for now
|
||||||
|
registry_server = urlparse.urlparse(request.url).netloc
|
||||||
|
response.headers['X-Docker-Endpoints'] = registry_server
|
||||||
|
|
||||||
has_token_request = request.headers.get('X-Docker-Token', '')
|
has_token_request = request.headers.get('X-Docker-Token', '')
|
||||||
|
|
||||||
|
|
48
endpoints/test.py
Normal file
48
endpoints/test.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
from random import SystemRandom
|
||||||
|
from flask import jsonify, send_file
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/test/build/status', methods=['GET'])
|
||||||
|
def generate_random_build_status():
|
||||||
|
response = {
|
||||||
|
'id': 1,
|
||||||
|
'total_commands': None,
|
||||||
|
'total_images': None,
|
||||||
|
'current_command': None,
|
||||||
|
'current_image': None,
|
||||||
|
'image_completion_percent': None,
|
||||||
|
'status': None,
|
||||||
|
'message': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
random = SystemRandom()
|
||||||
|
phases = {
|
||||||
|
'waiting': {},
|
||||||
|
'starting': {
|
||||||
|
'total_commands': 7,
|
||||||
|
'current_command': 0,
|
||||||
|
},
|
||||||
|
'initializing': {},
|
||||||
|
'error': {
|
||||||
|
'message': 'Oops!'
|
||||||
|
},
|
||||||
|
'complete': {},
|
||||||
|
'building': {
|
||||||
|
'total_commands': 7,
|
||||||
|
'current_command': random.randint(1, 7),
|
||||||
|
},
|
||||||
|
'pushing': {
|
||||||
|
'total_commands': 7,
|
||||||
|
'current_command': 7,
|
||||||
|
'total_images': 11,
|
||||||
|
'current_image': random.randint(1, 11),
|
||||||
|
'image_completion_percent': random.randint(0, 100),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
phase = random.choice(phases.keys())
|
||||||
|
response['status'] = phase
|
||||||
|
response.update(phases[phase])
|
||||||
|
|
||||||
|
return jsonify(response)
|
|
@ -67,6 +67,11 @@ def signin():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/new/')
|
||||||
|
def new():
|
||||||
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/repository/')
|
@app.route('/repository/')
|
||||||
def repository():
|
def repository():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
18
initdb.py
18
initdb.py
|
@ -5,6 +5,7 @@ import os
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
import storage
|
import storage
|
||||||
|
|
||||||
|
@ -95,6 +96,8 @@ def __generate_repository(user, name, description, is_public, permissions,
|
||||||
|
|
||||||
create_subtree(repo, structure, None)
|
create_subtree(repo, structure, None)
|
||||||
|
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
initialize_db()
|
initialize_db()
|
||||||
|
@ -141,6 +144,15 @@ if __name__ == '__main__':
|
||||||
'Shared repository, another user can write.', False,
|
'Shared repository, another user can write.', False,
|
||||||
[(new_user_2, 'write')], (5, [], 'latest'))
|
[(new_user_2, 'write')], (5, [], 'latest'))
|
||||||
|
|
||||||
__generate_repository(new_user_1, 'empty',
|
building = __generate_repository(new_user_1, 'building',
|
||||||
'Empty repository with no images or tags.', False,
|
'Empty repository which is building.',
|
||||||
[], (0, [], None))
|
False, [], (0, [], None))
|
||||||
|
|
||||||
|
token = model.create_access_token(building, 'write')
|
||||||
|
tag = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name)
|
||||||
|
build = model.create_repository_build(building, token, '123-45-6789', tag)
|
||||||
|
|
||||||
|
build.build_node_id = 1
|
||||||
|
build.phase = 'building'
|
||||||
|
build.status_url = 'http://localhost:5000/test/build/status'
|
||||||
|
build.save()
|
||||||
|
|
|
@ -10,6 +10,7 @@ events {
|
||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
|
types_hash_max_size 2048;
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
@ -36,8 +37,8 @@ http {
|
||||||
keepalive_timeout 5;
|
keepalive_timeout 5;
|
||||||
|
|
||||||
ssl on;
|
ssl on;
|
||||||
ssl_certificate /home/ubuntu/quay/certs/unified.cert;
|
ssl_certificate ./certs/quay-unified.cert;
|
||||||
ssl_certificate_key /home/ubuntu/quay/certs/quay.key;
|
ssl_certificate_key ./certs/quay.key;
|
||||||
ssl_session_timeout 5m;
|
ssl_session_timeout 5m;
|
||||||
ssl_protocols SSLv3 TLSv1;
|
ssl_protocols SSLv3 TLSv1;
|
||||||
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
||||||
|
|
|
@ -14,4 +14,6 @@ mixpanel-py
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
marisa-trie
|
marisa-trie
|
||||||
apscheduler
|
apscheduler
|
||||||
python-daemon
|
python-daemon
|
||||||
|
paramiko
|
||||||
|
python-digitalocean
|
|
@ -12,6 +12,7 @@ beautifulsoup4==4.3.2
|
||||||
blinker==1.3
|
blinker==1.3
|
||||||
boto==2.15.0
|
boto==2.15.0
|
||||||
distribute==0.6.34
|
distribute==0.6.34
|
||||||
|
ecdsa==0.10
|
||||||
eventlet==0.14.0
|
eventlet==0.14.0
|
||||||
greenlet==0.4.1
|
greenlet==0.4.1
|
||||||
gunicorn==18.0
|
gunicorn==18.0
|
||||||
|
@ -19,10 +20,13 @@ 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
|
||||||
peewee==2.1.4
|
peewee==2.1.4
|
||||||
py-bcrypt==0.4
|
py-bcrypt==0.4
|
||||||
|
pycrypto==2.6.1
|
||||||
python-daemon==1.6
|
python-daemon==1.6
|
||||||
python-dateutil==2.1
|
python-dateutil==2.1
|
||||||
|
python-digitalocean==0.5
|
||||||
requests==2.0.0
|
requests==2.0.0
|
||||||
six==1.4.1
|
six==1.4.1
|
||||||
stripe==1.9.8
|
stripe==1.9.8
|
||||||
|
|
|
@ -36,13 +36,57 @@
|
||||||
100% { -webkit-transform: scale(1); }
|
100% { -webkit-transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes scaleup {
|
@keyframes scaleup {
|
||||||
0% { transform: scale(0); }
|
0% { transform: scale(0); }
|
||||||
100% { transform: scale(1); }
|
100% { transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-tools .user-tool {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-top: 14px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tools i.user-tool:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #428bca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-boxes .popover {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-boxes .popover-content {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-statuses {
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-status-container {
|
||||||
|
padding: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
width: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-status-container .build-message {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-status-container .progress {
|
||||||
|
height: 12px;
|
||||||
|
margin: 0px;
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-status-container:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
border-bottom: 0px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-circle {
|
.repo-circle {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -57,15 +101,23 @@
|
||||||
|
|
||||||
.repo-circle.no-background {
|
.repo-circle.no-background {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-circle .icon-lock {
|
.repo-circle .fa-hdd {
|
||||||
font-size: 50%;
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-circle.no-background .fa-hdd {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-circle .fa-lock {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -6px;
|
bottom: -2px;
|
||||||
right: 0px;
|
right: -4px;
|
||||||
background: rgb(253, 191, 191);
|
background: rgb(253, 191, 191);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -73,11 +125,11 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
font-size: 12px;
|
font-size: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-circle.no-background .icon-lock {
|
.repo-circle.no-background .fa-lock {
|
||||||
bottom: -4px;
|
bottom: -2px;
|
||||||
right: -6px;
|
right: -6px;
|
||||||
color: #444;
|
color: #444;
|
||||||
}
|
}
|
||||||
|
@ -117,6 +169,85 @@
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-repo .required-plan {
|
||||||
|
margin: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .required-plan .alert {
|
||||||
|
color: #444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .new-header {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .new-header .repo-circle {
|
||||||
|
margin-right: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .new-header .name-container {
|
||||||
|
display: inline-block;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .description {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .section {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .repo-option {
|
||||||
|
margin: 6px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .repo-option i {
|
||||||
|
font-size: 18px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
width: 42px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .option-description {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .option-description label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .cbox {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .initialize-repo {
|
||||||
|
margin: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-left: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .initialize-repo .init-description {
|
||||||
|
color: #444;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo .initialize-repo .file-drop {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.user-guide h3 {
|
.user-guide h3 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
@ -532,6 +663,7 @@ p.editable:hover i {
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .description {
|
.repo .description {
|
||||||
|
margin-top: 10px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -561,22 +693,70 @@ p.editable:hover i {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo .status-boxes {
|
||||||
|
float: right;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .status-boxes .status-box {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .status-boxes .status-box .title {
|
||||||
|
padding: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .status-boxes .status-box .title i {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .status-boxes .status-box .count {
|
||||||
|
display: inline-block;
|
||||||
|
background-image: linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
transform: scaleX(0);
|
||||||
|
-webkit-transform: scaleX(0);
|
||||||
|
-moz-transform: scaleX(0);
|
||||||
|
|
||||||
|
transition: transform 500ms ease-in-out;
|
||||||
|
-webkit-transition: -webkit-transform 500ms ease-in-out;
|
||||||
|
-moz-transition: -moz-transform 500ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .status-boxes .status-box .count.visible {
|
||||||
|
transform: scaleX(1);
|
||||||
|
-webkit-transform: scaleX(1);
|
||||||
|
-moz-transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
.repo .pull-command {
|
.repo .pull-command {
|
||||||
float: right;
|
float: right;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 1.2em;
|
font-size: 0.8em;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: 10px;
|
margin-top: 30px;
|
||||||
|
margin-right: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .pull-command .pull-container {
|
.repo .pull-container {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .pull-command input {
|
.repo .pull-container input {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
background: white;
|
background: white;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
@ -702,7 +882,6 @@ p.editable:hover i {
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-listing i {
|
.repo-listing i {
|
||||||
font-size: 1.5em;
|
|
||||||
color: #999;
|
color: #999;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
@ -742,8 +921,22 @@ p.editable:hover i {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .description p {
|
|
||||||
margin-bottom: 6px;
|
.repo .build-info {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .build-info .progress {
|
||||||
|
margin: 0px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .section {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .description p:last-child {
|
.repo .description p:last-child {
|
||||||
|
@ -793,15 +986,15 @@ p.editable:hover i {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .changes-container i.icon-plus-sign-alt {
|
.repo .changes-container i.fa-plus-square {
|
||||||
color: rgb(73, 209, 73);
|
color: rgb(73, 209, 73);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .changes-container i.icon-minus-sign-alt {
|
.repo .changes-container i.fa-minus-square {
|
||||||
color: rgb(209, 73, 73);
|
color: rgb(209, 73, 73);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .changes-container i.icon-edit-sign {
|
.repo .changes-container i.fa-pencil-square {
|
||||||
color: rgb(73, 100, 209);
|
color: rgb(73, 100, 209);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -929,11 +1122,11 @@ p.editable:hover i {
|
||||||
width: 580px;
|
width: 580px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-admin .repo-access-state .state-icon i.icon-lock {
|
.repo-admin .repo-access-state .state-icon i.fa-lock {
|
||||||
background: rgb(253, 191, 191);
|
background: rgb(253, 191, 191);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-admin .repo-access-state .state-icon i.icon-unlock-alt {
|
.repo-admin .repo-access-state .state-icon i.fa-unlock {
|
||||||
background: rgb(170, 236, 170);
|
background: rgb(170, 236, 170);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1105,6 +1298,7 @@ p.editable:hover i {
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overrides for typeahead to work with bootstrap 3. */
|
/* Overrides for typeahead to work with bootstrap 3. */
|
||||||
|
|
8
static/directives/build-status.html
Normal file
8
static/directives/build-status.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<div id="build-status-container" class="build-status-container">
|
||||||
|
<span class="build-message">{{ getBuildMessage(build) }}</span>
|
||||||
|
<div class="progress" ng-class="getBuildProgress(build) < 100 ? 'active progress-striped' : ''" ng-show="getBuildProgress(build) >= 0">
|
||||||
|
<div class="progress-bar" role="progressbar" aria-valuenow="{{ getBuildProgress(build) }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + getBuildProgress(build) + '%' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,2 +1,2 @@
|
||||||
<i class="icon-lock icon-large" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: visible' }}" title="Private Repository"></i>
|
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: visible' }}" title="Private Repository"></i>
|
||||||
<i class="icon-hdd icon-large"></i>
|
<i class="fa fa-hdd"></i>
|
||||||
|
|
120
static/js/app.js
120
static/js/app.js
|
@ -1,5 +1,5 @@
|
||||||
// Start the application code itself.
|
// Start the application code itself.
|
||||||
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel'], function($provide) {
|
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives'], function($provide) {
|
||||||
$provide.factory('UserService', ['Restangular', function(Restangular) {
|
$provide.factory('UserService', ['Restangular', function(Restangular) {
|
||||||
var userResponse = {
|
var userResponse = {
|
||||||
verified: false,
|
verified: false,
|
||||||
|
@ -54,7 +54,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
return keyService;
|
return keyService;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
$provide.factory('PlanService', [function() {
|
$provide.factory('PlanService', ['Restangular', 'KeyService', function(Restangular, KeyService) {
|
||||||
var plans = [
|
var plans = [
|
||||||
{
|
{
|
||||||
title: 'Open Source',
|
title: 'Open Source',
|
||||||
|
@ -96,11 +96,54 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
|
|
||||||
planService.planList = function() {
|
planService.planList = function() {
|
||||||
return plans;
|
return plans;
|
||||||
}
|
};
|
||||||
|
|
||||||
planService.getPlan = function(planId) {
|
planService.getPlan = function(planId) {
|
||||||
return planDict[planId];
|
return planDict[planId];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
planService.getMinimumPlan = function(privateCount) {
|
||||||
|
for (var i = 0; i < plans.length; i++) {
|
||||||
|
var plan = plans[i];
|
||||||
|
if (plan.privateRepos >= privateCount) {
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.showSubscribeDialog = function($scope, planId, started, success, failed) {
|
||||||
|
var submitToken = function(token) {
|
||||||
|
$scope.$apply(function() {
|
||||||
|
started();
|
||||||
|
});
|
||||||
|
|
||||||
|
mixpanel.track('plan_subscribe');
|
||||||
|
|
||||||
|
var subscriptionDetails = {
|
||||||
|
token: token.id,
|
||||||
|
plan: planId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var createSubscriptionRequest = Restangular.one('user/plan');
|
||||||
|
$scope.$apply(function() {
|
||||||
|
createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failed);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var planDetails = planService.getPlan(planId)
|
||||||
|
StripeCheckout.open({
|
||||||
|
key: KeyService.stripePublishableKey,
|
||||||
|
address: false, // TODO change to true
|
||||||
|
amount: planDetails.price,
|
||||||
|
currency: 'usd',
|
||||||
|
name: 'Quay ' + planDetails.title + ' Subscription',
|
||||||
|
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
|
||||||
|
panelLabel: 'Subscribe',
|
||||||
|
token: submitToken
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return planService;
|
return planService;
|
||||||
}]);
|
}]);
|
||||||
|
@ -155,6 +198,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
when('/guide/', {title: 'User Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
|
when('/guide/', {title: 'User Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
|
||||||
when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
||||||
when('/signin/', {title: 'Signin', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
|
when('/signin/', {title: 'Signin', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
|
||||||
|
when('/new/', {title: 'Create new repository', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
|
||||||
|
|
||||||
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}).
|
||||||
|
|
||||||
|
@ -176,7 +220,73 @@ quayApp.directive('repoCircle', function () {
|
||||||
'repo': '=repo'
|
'repo': '=repo'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element) {
|
||||||
window.console.log($scope);
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('buildStatus', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/build-status.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'build': '=build'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element) {
|
||||||
|
$scope.getBuildProgress = function(buildInfo) {
|
||||||
|
switch (buildInfo.status) {
|
||||||
|
case 'building':
|
||||||
|
return (buildInfo.current_command / buildInfo.total_commands) * 100;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pushing':
|
||||||
|
var imagePercentDecimal = (buildInfo.image_completion_percent / 100);
|
||||||
|
return ((buildInfo.current_image + imagePercentDecimal) / buildInfo.total_images) * 100;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'complete':
|
||||||
|
return 100;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'initializing':
|
||||||
|
case 'starting':
|
||||||
|
case 'waiting':
|
||||||
|
return 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getBuildMessage = function(buildInfo) {
|
||||||
|
switch (buildInfo.status) {
|
||||||
|
case 'initializing':
|
||||||
|
return 'Starting Dockerfile build';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'starting':
|
||||||
|
case 'waiting':
|
||||||
|
case 'building':
|
||||||
|
return 'Building image from Dockerfile';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pushing':
|
||||||
|
return 'Pushing image built from Dockerfile';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'complete':
|
||||||
|
return 'Dockerfile build completed and pushed';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
return 'Dockerfile build failed: ' + buildInfo.message;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
|
|
@ -92,7 +92,7 @@ function HeaderCtrl($scope, $location, UserService, Restangular) {
|
||||||
},
|
},
|
||||||
template: function (datum) {
|
template: function (datum) {
|
||||||
template = '<div class="repo-mini-listing">';
|
template = '<div class="repo-mini-listing">';
|
||||||
template += '<i class="icon-hdd icon-large"></i>'
|
template += '<i class="fa fa-hdd fa-lg"></i>'
|
||||||
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
|
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
|
||||||
if (datum.repo.description) {
|
if (datum.repo.description) {
|
||||||
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
|
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
|
||||||
|
@ -211,7 +211,7 @@ function RepoListCtrl($scope, Restangular, UserService) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function LandingCtrl($scope, $timeout, Restangular, UserService, KeyService) {
|
function LandingCtrl($scope, $timeout, $location, Restangular, UserService, KeyService) {
|
||||||
$('.form-signup').popover();
|
$('.form-signup').popover();
|
||||||
|
|
||||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
|
@ -236,10 +236,6 @@ function LandingCtrl($scope, $timeout, Restangular, UserService, KeyService) {
|
||||||
return getMarkedDown(getFirstTextLine(commentString));
|
return getMarkedDown(getFirstTextLine(commentString));
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.browseRepos = function() {
|
|
||||||
document.location = '/repository/';
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.register = function() {
|
$scope.register = function() {
|
||||||
$('.form-signup').popover('hide');
|
$('.form-signup').popover('hide');
|
||||||
$scope.registering = true;
|
$scope.registering = true;
|
||||||
|
@ -346,12 +342,83 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var listImages = function() {
|
var fetchRepository = function() {
|
||||||
if ($scope.imageHistory) { return; }
|
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
|
||||||
|
repositoryFetch.get().then(function(repo) {
|
||||||
|
$rootScope.title = namespace + '/' + name;
|
||||||
|
$scope.repo = repo;
|
||||||
|
|
||||||
|
$scope.setTag($routeParams.tag);
|
||||||
|
|
||||||
|
$('#copyClipboard').clipboardCopy();
|
||||||
|
$scope.loading = false;
|
||||||
|
|
||||||
|
if (repo.is_building) {
|
||||||
|
startBuildInfoTimer(repo);
|
||||||
|
}
|
||||||
|
}, function() {
|
||||||
|
$scope.repo = null;
|
||||||
|
$scope.loading = false;
|
||||||
|
$rootScope.title = 'Unknown Repository';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var startBuildInfoTimer = function(repo) {
|
||||||
|
if ($scope.interval) { return; }
|
||||||
|
|
||||||
|
getBuildInfo(repo);
|
||||||
|
$scope.interval = setInterval(function() {
|
||||||
|
$scope.$apply(function() { getBuildInfo(repo); });
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
$scope.$on("$destroy", function() {
|
||||||
|
cancelBuildInfoTimer();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var cancelBuildInfoTimer = function() {
|
||||||
|
if ($scope.interval) {
|
||||||
|
clearInterval($scope.interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var getBuildInfo = function(repo) {
|
||||||
|
var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/');
|
||||||
|
buildInfo.get().then(function(resp) {
|
||||||
|
var runningBuilds = [];
|
||||||
|
for (var i = 0; i < resp.builds.length; ++i) {
|
||||||
|
var build = resp.builds[i];
|
||||||
|
if (build.status != 'complete') {
|
||||||
|
runningBuilds.push(build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.buildsInfo = runningBuilds;
|
||||||
|
if (!runningBuilds.length) {
|
||||||
|
// Cancel the build timer.
|
||||||
|
cancelBuildInfoTimer();
|
||||||
|
|
||||||
|
// Mark the repo as no longer building.
|
||||||
|
$scope.repo.is_building = false;
|
||||||
|
|
||||||
|
// Reload the repo information.
|
||||||
|
fetchRepository();
|
||||||
|
listImages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var listImages = function() {
|
||||||
var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/');
|
var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/');
|
||||||
imageFetch.get().then(function(resp) {
|
imageFetch.get().then(function(resp) {
|
||||||
$scope.imageHistory = resp.images;
|
$scope.imageHistory = resp.images;
|
||||||
|
|
||||||
|
// Dispose of any existing tree.
|
||||||
|
if ($scope.tree) {
|
||||||
|
$scope.tree.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new tree.
|
||||||
$scope.tree = new ImageHistoryTree(namespace, name, resp.images,
|
$scope.tree = new ImageHistoryTree(namespace, name, resp.images,
|
||||||
$scope.getCommentFirstLine, $scope.getTimeSince);
|
$scope.getCommentFirstLine, $scope.getTimeSince);
|
||||||
|
|
||||||
|
@ -363,7 +430,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
||||||
}
|
}
|
||||||
|
|
||||||
$($scope.tree).bind('tagChanged', function(e) {
|
$($scope.tree).bind('tagChanged', function(e) {
|
||||||
$scope.$apply(function() { $scope.setTag(e.tag, true); });
|
$scope.$apply(function() { $scope.setTag(e.tag, true); });
|
||||||
});
|
});
|
||||||
$($scope.tree).bind('imageChanged', function(e) {
|
$($scope.tree).bind('imageChanged', function(e) {
|
||||||
$scope.$apply(function() { $scope.setImage(e.image); });
|
$scope.$apply(function() { $scope.setImage(e.image); });
|
||||||
|
@ -378,7 +445,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
||||||
changesFetch.get().then(function(changeInfo) {
|
changesFetch.get().then(function(changeInfo) {
|
||||||
$scope.currentImageChanges = changeInfo;
|
$scope.currentImageChanges = changeInfo;
|
||||||
}, function() {
|
}, function() {
|
||||||
$scope.currentImageChanges = {};
|
$scope.currentImageChanges = {'added': [], 'removed': [], 'changed': []};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -441,20 +508,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
|
|
||||||
// Fetch the repo.
|
// Fetch the repo.
|
||||||
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
|
fetchRepository();
|
||||||
repositoryFetch.get().then(function(repo) {
|
|
||||||
$rootScope.title = namespace + '/' + name;
|
|
||||||
$scope.repo = repo;
|
|
||||||
|
|
||||||
$scope.setTag($routeParams.tag);
|
|
||||||
|
|
||||||
$('#copyClipboard').clipboardCopy();
|
|
||||||
$scope.loading = false;
|
|
||||||
}, function() {
|
|
||||||
$scope.repo = null;
|
|
||||||
$scope.loading = false;
|
|
||||||
$rootScope.title = 'Unknown Repository';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch the image history.
|
// Fetch the image history.
|
||||||
listImages();
|
listImages();
|
||||||
|
@ -492,7 +546,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
},
|
},
|
||||||
template: function (datum) {
|
template: function (datum) {
|
||||||
template = '<div class="user-mini-listing">';
|
template = '<div class="user-mini-listing">';
|
||||||
template += '<i class="icon-user icon-large"></i>'
|
template += '<i class="fa fa-user fa-lg"></i>'
|
||||||
template += '<span class="name">' + datum.username + '</span>'
|
template += '<span class="name">' + datum.username + '</span>'
|
||||||
template += '</div>'
|
template += '</div>'
|
||||||
return template;
|
return template;
|
||||||
|
@ -702,6 +756,8 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
||||||
|
|
||||||
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
|
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
|
||||||
$scope.errorMessage = 'You are using more private repositories than your plan allows, please upgrate your subscription to avoid disruptions in your service.';
|
$scope.errorMessage = 'You are using more private repositories than your plan allows, please upgrate your subscription to avoid disruptions in your service.';
|
||||||
|
} else {
|
||||||
|
$scope.errorMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.planLoading = false;
|
$scope.planLoading = false;
|
||||||
|
@ -710,7 +766,7 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
||||||
mixpanel.people.set({
|
mixpanel.people.set({
|
||||||
'plan': sub.plan
|
'plan': sub.plan
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
$scope.planLoading = true;
|
$scope.planLoading = true;
|
||||||
var getSubscription = Restangular.one('user/plan');
|
var getSubscription = Restangular.one('user/plan');
|
||||||
|
@ -721,36 +777,16 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
||||||
|
|
||||||
$scope.planChanging = false;
|
$scope.planChanging = false;
|
||||||
$scope.subscribe = function(planId) {
|
$scope.subscribe = function(planId) {
|
||||||
var submitToken = function(token) {
|
PlanService.showSubscribeDialog($scope, planId, function() {
|
||||||
$scope.$apply(function() {
|
// Subscribing.
|
||||||
mixpanel.track('plan_subscribe');
|
$scope.planChanging = true;
|
||||||
|
}, function(plan) {
|
||||||
$scope.planChanging = true;
|
// Subscribed.
|
||||||
$scope.errorMessage = undefined;
|
subscribedToPlan(plan);
|
||||||
|
}, function() {
|
||||||
var subscriptionDetails = {
|
// Failure.
|
||||||
token: token.id,
|
$scope.errorMessage = 'Unable to subscribe.';
|
||||||
plan: planId,
|
$scope.planChanging = false;
|
||||||
};
|
|
||||||
|
|
||||||
var createSubscriptionRequest = Restangular.one('user/plan');
|
|
||||||
createSubscriptionRequest.customPUT(subscriptionDetails).then(subscribedToPlan, function() {
|
|
||||||
// Failure
|
|
||||||
$scope.errorMessage = 'Unable to subscribe.';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var planDetails = PlanService.getPlan(planId)
|
|
||||||
StripeCheckout.open({
|
|
||||||
key: KeyService.stripePublishableKey,
|
|
||||||
address: false, // TODO change to true
|
|
||||||
amount: planDetails.price,
|
|
||||||
currency: 'usd',
|
|
||||||
name: 'Quay ' + planDetails.title + ' Subscription',
|
|
||||||
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
|
|
||||||
panelLabel: 'Subscribe',
|
|
||||||
token: submitToken
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -911,12 +947,182 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function V1Ctrl($scope, UserService) {
|
function V1Ctrl($scope, $location, UserService) {
|
||||||
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
|
$scope.user = currentUser;
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewRepoCtrl($scope, $location, $http, UserService, Restangular, PlanService) {
|
||||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
$scope.user = currentUser;
|
$scope.user = currentUser;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
$scope.browseRepos = function() {
|
$scope.repo = {
|
||||||
document.location = '/repository/';
|
'is_public': 1,
|
||||||
|
'description': '',
|
||||||
|
'initialize': false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$('#couldnotbuildModal').on('hidden.bs.modal', function() {
|
||||||
|
$scope.$apply(function() {
|
||||||
|
$location.path('/repository/' + $scope.created.namespace + '/' + $scope.created.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var startBuild = function(repo, fileId) {
|
||||||
|
$scope.building = true;
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'file_id': fileId
|
||||||
|
};
|
||||||
|
|
||||||
|
var startBuildCall = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/');
|
||||||
|
startBuildCall.customPOST(data).then(function(resp) {
|
||||||
|
$location.path('/repository/' + repo.namespace + '/' + repo.name);
|
||||||
|
}, function() {
|
||||||
|
$('#couldnotbuildModal').modal();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var conductUpload = function(repo, file, url, fileId, mimeType) {
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
request.open('PUT', url, true);
|
||||||
|
request.setRequestHeader('Content-Type', mimeType);
|
||||||
|
request.onprogress = function(e) {
|
||||||
|
$scope.$apply(function() {
|
||||||
|
var percentLoaded;
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
$scope.upload_progress = (e.loaded / e.total) * 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
request.onerror = function() {
|
||||||
|
$scope.$apply(function() {
|
||||||
|
$('#couldnotbuildModal').modal();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
request.onreadystatechange = function() {
|
||||||
|
var state = request.readyState;
|
||||||
|
if (state == 4) {
|
||||||
|
$scope.$apply(function() {
|
||||||
|
$scope.uploading = false;
|
||||||
|
startBuild(repo, fileId);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request.send(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
var startFileUpload = function(repo) {
|
||||||
|
$scope.uploading = true;
|
||||||
|
$scope.uploading_progress = 0;
|
||||||
|
|
||||||
|
var uploader = $('#file-drop')[0];
|
||||||
|
var file = uploader.files[0];
|
||||||
|
$scope.upload_file = file.name;
|
||||||
|
|
||||||
|
var mimeType = file.type || 'application/octet-stream';
|
||||||
|
var data = {
|
||||||
|
'mimeType': mimeType
|
||||||
|
};
|
||||||
|
|
||||||
|
var getUploadUrl = Restangular.one('filedrop/');
|
||||||
|
getUploadUrl.customPOST(data).then(function(resp) {
|
||||||
|
conductUpload(repo, file, resp.url, resp.file_id, mimeType);
|
||||||
|
}, function() {
|
||||||
|
$('#couldnotbuildModal').modal();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var subscribedToPlan = function(sub) {
|
||||||
|
$scope.planChanging = false;
|
||||||
|
$scope.subscription = sub;
|
||||||
|
$scope.subscribedPlan = PlanService.getPlan(sub.plan);
|
||||||
|
$scope.planRequired = null;
|
||||||
|
if ($scope.subscription.usedPrivateRepos >= $scope.subscribedPlan.privateRepos) {
|
||||||
|
$scope.planRequired = PlanService.getMinimumPlan($scope.subscription.usedPrivateRepos);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.editDescription = function() {
|
||||||
|
if (!$scope.markdownDescriptionEditor) {
|
||||||
|
var converter = Markdown.getSanitizingConverter();
|
||||||
|
var editor = new Markdown.Editor(converter, '-description');
|
||||||
|
editor.run();
|
||||||
|
$scope.markdownDescriptionEditor = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#wmd-input-description')[0].value = $scope.repo.description;
|
||||||
|
$('#editModal').modal({});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getMarkedDown = function(string) {
|
||||||
|
if (!string) { return ''; }
|
||||||
|
return getMarkedDown(string);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveDescription = function() {
|
||||||
|
$('#editModal').modal('hide');
|
||||||
|
$scope.repo.description = $('#wmd-input-description')[0].value;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.createNewRepo = function() {
|
||||||
|
var uploader = $('#file-drop')[0];
|
||||||
|
if ($scope.repo.initialize && uploader.files.length < 1) {
|
||||||
|
$('#missingfileModal').modal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.creating = true;
|
||||||
|
var repo = $scope.repo;
|
||||||
|
var data = {
|
||||||
|
'repository': repo.name,
|
||||||
|
'visibility': repo.is_public == '1' ? 'public' : 'private',
|
||||||
|
'description': repo.description
|
||||||
|
};
|
||||||
|
|
||||||
|
var createPost = Restangular.one('repository');
|
||||||
|
createPost.customPOST(data).then(function(created) {
|
||||||
|
$scope.creating = false;
|
||||||
|
$scope.created = created;
|
||||||
|
|
||||||
|
// Repository created. Start the upload process if applicable.
|
||||||
|
if ($scope.repo.initialize) {
|
||||||
|
startFileUpload(created);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, redirect to the repo page.
|
||||||
|
$location.path('/repository/' + created.namespace + '/' + created.name);
|
||||||
|
}, function() {
|
||||||
|
$('#cannotcreateModal').modal();
|
||||||
|
$scope.creating = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.upgradePlan = function() {
|
||||||
|
PlanService.showSubscribeDialog($scope, $scope.planRequired.stripeId, function() {
|
||||||
|
// Subscribing.
|
||||||
|
$scope.planChanging = true;
|
||||||
|
}, function(plan) {
|
||||||
|
// Subscribed.
|
||||||
|
subscribedToPlan(plan);
|
||||||
|
}, function() {
|
||||||
|
// Failure.
|
||||||
|
$('#couldnotsubscribeModal').modal();
|
||||||
|
$scope.planChanging = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.plans = PlanService.planList();
|
||||||
|
|
||||||
|
// Load the user's subscription information in case they want to create a private
|
||||||
|
// repository.
|
||||||
|
var getSubscription = Restangular.one('user/plan');
|
||||||
|
getSubscription.get().then(subscribedToPlan, function() {
|
||||||
|
// User has no subscription
|
||||||
|
$scope.planRequired = PlanService.getMinimumPlan(1);
|
||||||
|
});
|
||||||
}
|
}
|
|
@ -715,7 +715,17 @@ ImageHistoryTree.prototype.toggle_ = function(d) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
/**
|
||||||
|
* Disposes of the tree.
|
||||||
|
*/
|
||||||
|
ImageHistoryTree.prototype.dispose = function() {
|
||||||
|
var container = this.container_ ;
|
||||||
|
$('#' + container).removeOverscroll();
|
||||||
|
document.getElementById(container).innerHTML = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based off of http://bl.ocks.org/mbostock/1093025 by Mike Bostock (@mbostock)
|
* Based off of http://bl.ocks.org/mbostock/1093025 by Mike Bostock (@mbostock)
|
||||||
|
@ -809,6 +819,15 @@ ImageFileChangeTree.prototype.notifyResized = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the tree.
|
||||||
|
*/
|
||||||
|
ImageFileChangeTree.prototype.dispose = function() {
|
||||||
|
var container = this.container_ ;
|
||||||
|
document.getElementById(container).innerHTML = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the tree.
|
* Draws the tree.
|
||||||
*/
|
*/
|
||||||
|
@ -1039,17 +1058,17 @@ ImageFileChangeTree.prototype.update_ = function(source) {
|
||||||
node.select('.node-icon')
|
node.select('.node-icon')
|
||||||
.html(function(d) {
|
.html(function(d) {
|
||||||
if (!d.kind) {
|
if (!d.kind) {
|
||||||
var folder = d._children ? 'icon-folder-close' : 'icon-folder-open';
|
var folder = d._children ? 'fa fa-folder' : 'fa fa-folder-open';
|
||||||
return '<i class="' + folder + '"></i>';
|
return '<i class="' + folder + '"></i>';
|
||||||
}
|
}
|
||||||
|
|
||||||
var icon = {
|
var icon = {
|
||||||
'added': 'plus-sign-alt',
|
'added': 'plus-square',
|
||||||
'removed': 'minus-sign-alt',
|
'removed': 'minus-square',
|
||||||
'changed': 'edit-sign'
|
'changed': 'pencil-square'
|
||||||
};
|
};
|
||||||
|
|
||||||
return '<i class="change-icon icon-' + icon[d.kind] + '"></i>';
|
return '<i class="change-icon fa fa-' + icon[d.kind] + '"></i>';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Transition exiting nodes to the parent's new position.
|
// Transition exiting nodes to the parent's new position.
|
||||||
|
|
8
static/lib/angular-strap.min.js
vendored
Normal file
8
static/lib/angular-strap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
||||||
(function(browserchrome, $) {
|
(function(browserchrome, $) {
|
||||||
var htmlTemplate = '<div class="browser-chrome-container"><div class="browser-chrome-header"><i class="icon-remove-sign"></i> <i class="icon-minus-sign"></i> <i class="icon-plus-sign"></i><div class="browser-chrome-tab"><div class="browser-chrome-tab-wrapper"><div class="browser-chrome-tab-content"><i class="icon-file-alt icon-large"></i> <span class="tab-title">Tab Title</span></div></div></div><div class="user-icon-container"><i class="icon-user icon-2x"></i></div></div><div class="browser-chrome-url-bar"><div class="left-controls"><i class="icon-arrow-left icon-large"></i> <i class="icon-arrow-right icon-large"></i> <i class="icon-rotate-right icon-large"></i> </div><div class="right-controls"> <i class="icon-reorder icon-large"></i></div><div class="browser-chrome-url"><span class="protocol-https" style="display: none"><i class="icon-lock"></i>https</span><span class="protocol-http"><i class="icon-file-alt"></i>http</span><span class="url-text">://google.com/</span></div></div></div>'
|
var htmlTemplate = '<div class="browser-chrome-container"><div class="browser-chrome-header"><i class="fa fa-times-circle"></i> <i class="fa fa-minus-circle"></i> <i class="fa fa-plus-circle"></i><div class="browser-chrome-tab"><div class="browser-chrome-tab-wrapper"><div class="browser-chrome-tab-content"><i class="fa fa-file-alt fa-lg"></i> <span class="tab-title">Tab Title</span></div></div></div><div class="user-icon-container"><i class="fa fa-user fa-2x"></i></div></div><div class="browser-chrome-url-bar"><div class="left-controls"><i class="fa fa-arrow-left fa-lg"></i> <i class="fa fa-arrow-right fa-lg"></i> <i class="fa fa-rotate-right fa-lg"></i> </div><div class="right-controls"> <i class="fa fa-reorder fa-lg"></i></div><div class="browser-chrome-url"><span class="protocol-https" style="display: none"><i class="fa fa-lock"></i>https</span><span class="protocol-http"><i class="fa fa-file-alt"></i>http</span><span class="url-text">://google.com/</span></div></div></div>'
|
||||||
|
|
||||||
browserchrome.update = function() {
|
browserchrome.update = function() {
|
||||||
$('[data-screenshot-url]').each(function(index, element) {
|
$('[data-screenshot-url]').each(function(index, element) {
|
||||||
|
|
|
@ -1359,42 +1359,42 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
group1 = makeGroup(1);
|
group1 = makeGroup(1);
|
||||||
buttons.bold = makeButton("wmd-bold-button", "Bold - Ctrl+B", "icon-bold", bindCommand("doBold"), group1);
|
buttons.bold = makeButton("wmd-bold-button", "Bold - Ctrl+B", "fa fa-bold", bindCommand("doBold"), group1);
|
||||||
buttons.italic = makeButton("wmd-italic-button", "Italic - Ctrl+I", "icon-italic", bindCommand("doItalic"), group1);
|
buttons.italic = makeButton("wmd-italic-button", "Italic - Ctrl+I", "fa fa-italic", bindCommand("doItalic"), group1);
|
||||||
|
|
||||||
group2 = makeGroup(2);
|
group2 = makeGroup(2);
|
||||||
/*
|
/*
|
||||||
buttons.link = makeButton("wmd-link-button", "Link - Ctrl+L", "icon-link", bindCommand(function (chunk, postProcessing) {
|
buttons.link = makeButton("wmd-link-button", "Link - Ctrl+L", "fa fa-link", bindCommand(function (chunk, postProcessing) {
|
||||||
return this.doLinkOrImage(chunk, postProcessing, false);
|
return this.doLinkOrImage(chunk, postProcessing, false);
|
||||||
}), group2);
|
}), group2);
|
||||||
*/
|
*/
|
||||||
buttons.quote = makeButton("wmd-quote-button", "Blockquote - Ctrl+Q", "icon-quote-left", bindCommand("doBlockquote"), group2);
|
buttons.quote = makeButton("wmd-quote-button", "Blockquote - Ctrl+Q", "fa fa-quote-left", bindCommand("doBlockquote"), group2);
|
||||||
buttons.code = makeButton("wmd-code-button", "Code Sample - Ctrl+K", "icon-code", bindCommand("doCode"), group2);
|
buttons.code = makeButton("wmd-code-button", "Code Sample - Ctrl+K", "fa fa-code", bindCommand("doCode"), group2);
|
||||||
/*
|
/*
|
||||||
buttons.image = makeButton("wmd-image-button", "Image - Ctrl+G", "icon-picture", bindCommand(function (chunk, postProcessing) {
|
buttons.image = makeButton("wmd-image-button", "Image - Ctrl+G", "fa fa-picture", bindCommand(function (chunk, postProcessing) {
|
||||||
return this.doLinkOrImage(chunk, postProcessing, true);
|
return this.doLinkOrImage(chunk, postProcessing, true);
|
||||||
}), group2);
|
}), group2);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
group3 = makeGroup(3);
|
group3 = makeGroup(3);
|
||||||
buttons.olist = makeButton("wmd-olist-button", "Numbered List - Ctrl+O", "icon-list", bindCommand(function (chunk, postProcessing) {
|
buttons.olist = makeButton("wmd-olist-button", "Numbered List - Ctrl+O", "fa fa-list", bindCommand(function (chunk, postProcessing) {
|
||||||
this.doList(chunk, postProcessing, true);
|
this.doList(chunk, postProcessing, true);
|
||||||
}), group3);
|
}), group3);
|
||||||
buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List - Ctrl+U", "icon-list-ul", bindCommand(function (chunk, postProcessing) {
|
buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List - Ctrl+U", "fa fa-list-ul", bindCommand(function (chunk, postProcessing) {
|
||||||
this.doList(chunk, postProcessing, false);
|
this.doList(chunk, postProcessing, false);
|
||||||
}), group3);
|
}), group3);
|
||||||
buttons.heading = makeButton("wmd-heading-button", "Heading - Ctrl+H", "icon-tasks", bindCommand("doHeading"), group3);
|
buttons.heading = makeButton("wmd-heading-button", "Heading - Ctrl+H", "fa fa-tasks", bindCommand("doHeading"), group3);
|
||||||
buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule - Ctrl+R", "icon-minus", bindCommand("doHorizontalRule"), group3);
|
buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule - Ctrl+R", "fa fa-minus", bindCommand("doHorizontalRule"), group3);
|
||||||
|
|
||||||
group4 = makeGroup(4);
|
group4 = makeGroup(4);
|
||||||
buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "icon-undo", null, group4);
|
buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "fa fa-undo", null, group4);
|
||||||
buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
|
buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
|
||||||
|
|
||||||
var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
|
var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
|
||||||
"Redo - Ctrl+Y" :
|
"Redo - Ctrl+Y" :
|
||||||
"Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms
|
"Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms
|
||||||
|
|
||||||
buttons.redo = makeButton("wmd-redo-button", redoTitle, "icon-share-alt", null, group4);
|
buttons.redo = makeButton("wmd-redo-button", redoTitle, "fa fa-share", null, group4);
|
||||||
buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
|
buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
|
||||||
|
|
||||||
if (helpOptions) {
|
if (helpOptions) {
|
||||||
|
@ -1402,7 +1402,7 @@
|
||||||
group5.className = group5.className + " pull-right";
|
group5.className = group5.className + " pull-right";
|
||||||
var helpButton = document.createElement("button");
|
var helpButton = document.createElement("button");
|
||||||
var helpButtonImage = document.createElement("i");
|
var helpButtonImage = document.createElement("i");
|
||||||
helpButtonImage.className = "icon-question-sign";
|
helpButtonImage.className = "fa fa-question-sign";
|
||||||
helpButton.appendChild(helpButtonImage);
|
helpButton.appendChild(helpButtonImage);
|
||||||
helpButton.className = "btn";
|
helpButton.className = "btn";
|
||||||
helpButton.id = "wmd-help-button" + postfix;
|
helpButton.id = "wmd-help-button" + postfix;
|
||||||
|
|
9
static/partials/build-status-item.html
Normal file
9
static/partials/build-status-item.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<div class="build-statuses">
|
||||||
|
<div ng-repeat="build in buildsInfo">
|
||||||
|
<div class="build-status" build="build"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="buildsInfo.length == 0">
|
||||||
|
All Dockerfile builds complete
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
|
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
|
||||||
<span class="sr-only">Toggle navigation</span>
|
<span class="sr-only">Toggle navigation</span>
|
||||||
<span class="icon-bar"></span>
|
<span class="fa-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="fa-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="fa-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
|
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
|
||||||
<img src="/static/img/quay-logo.png">
|
<img src="/static/img/quay-logo.png">
|
||||||
|
@ -27,9 +27,11 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<li class="dropdown" ng-switch-when="false">
|
<span class="navbar-left user-tools" ng-show="!user.anonymous">
|
||||||
<!--<button type="button" class="btn btn-default navbar-btn">Sign in</button>-->
|
<a href="/new/"><i class="fa fa-upload user-tool" title="Create new repository"></i></a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<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">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
|
|
|
@ -3,14 +3,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="loading" ng-show="loading">
|
<div class="loading" ng-show="loading">
|
||||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container repo repo-image-view" ng-show="!loading && image">
|
<div class="container repo repo-image-view" ng-show="!loading && image">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="icon-chevron-left"></i></a>
|
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||||
<h3>
|
<h3>
|
||||||
<i class="icon-archive icon-large" style="color: #aaa; margin-right: 10px;"></i>
|
<i class="fa fa-archive fa-lg" style="color: #aaa; margin-right: 10px;"></i>
|
||||||
<span style="color: #aaa;"> {{repo.namespace}}</span>
|
<span style="color: #aaa;"> {{repo.namespace}}</span>
|
||||||
<span style="color: #ccc">/</span>
|
<span style="color: #ccc">/</span>
|
||||||
<span style="color: #666;">{{repo.name}}</span>
|
<span style="color: #666;">{{repo.name}}</span>
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="full-id" type="text" class="form-control" value="{{ image.id }}" readonly>
|
<input id="full-id" type="text" class="form-control" value="{{ image.id }}" readonly>
|
||||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="full-id">
|
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="full-id">
|
||||||
<i class="icon-copy"></i>
|
<i class="fa fa-copy"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
No matching changes
|
No matching changes
|
||||||
</div>
|
</div>
|
||||||
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
|
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
|
||||||
<i ng-class="{'added': 'icon-plus-sign-alt', 'removed': 'icon-minus-sign-alt', 'changed': 'icon-edit-sign'}[change.kind]"></i>
|
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
|
||||||
<span title="{{change.file}}">
|
<span title="{{change.file}}">
|
||||||
<span style="color: #888;">
|
<span style="color: #888;">
|
||||||
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
|
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<div ng-show="!user.anonymous">
|
<div ng-show="!user.anonymous">
|
||||||
<div ng-show="loadingmyrepos">
|
<div ng-show="loadingmyrepos">
|
||||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="!loadingmyrepos && myrepos.length > 0">
|
<div ng-show="!loadingmyrepos && myrepos.length > 0">
|
||||||
<h2>Your Top Repositories</h2>
|
<h2>Your Top Repositories</h2>
|
||||||
|
@ -44,10 +44,10 @@
|
||||||
<div class="form-group signin-buttons">
|
<div class="form-group signin-buttons">
|
||||||
<button class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit" analytics-on analytics-event="register">Sign Up for Free!</button>
|
<button class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit" analytics-on analytics-event="register">Sign Up for Free!</button>
|
||||||
<span class="landing-social-alternate">
|
<span class="landing-social-alternate">
|
||||||
<i class="icon-circle"></i>
|
<i class="fa fa-circle"></i>
|
||||||
<span class="inner-text">OR</span>
|
<span class="inner-text">OR</span>
|
||||||
</span>
|
</span>
|
||||||
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}" class="btn btn-primary btn-block"><i class="icon-github icon-large"></i> Sign In with GitHub</a>
|
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}" class="btn btn-primary btn-block"><i class="fa fa-github fa-lg"></i> Sign In with GitHub</a>
|
||||||
<p class="help-block">No credit card required.</p>
|
<p class="help-block">No credit card required.</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -61,14 +61,15 @@
|
||||||
<div ng-show="!user.anonymous" class="user-welcome">
|
<div ng-show="!user.anonymous" class="user-welcome">
|
||||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
||||||
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
||||||
<button ng-show="myrepos" class="btn btn-lg btn-primary btn-block" ng-click="browseRepos()">Browse all repositories</button>
|
<a ng-show="myrepos" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
|
<a ng-show="myrepos" class="btn btn-success" href="/new/">Create a new repository</a>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- col -->
|
</div> <!-- col -->
|
||||||
</div> <!-- row -->
|
</div> <!-- row -->
|
||||||
|
|
||||||
<div class="row" ng-show="user.anonymous">
|
<div class="row" ng-show="user.anonymous">
|
||||||
<div class="col-md-4 shoutout">
|
<div class="col-md-4 shoutout">
|
||||||
<i class="icon-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
|
Store your private docker containers where only you and your team
|
||||||
|
@ -77,7 +78,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4 shoutout">
|
<div class="col-md-4 shoutout">
|
||||||
<i class="icon-user"></i>
|
<i class="fa fa-user"></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
|
||||||
|
@ -85,7 +86,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4 shoutout">
|
<div class="col-md-4 shoutout">
|
||||||
<i class="icon-cloud"></i>
|
<i class="fa fa-cloud"></i>
|
||||||
<b>Cloud Hosted</b>
|
<b>Cloud Hosted</b>
|
||||||
<span class="shoutout-expand">
|
<span class="shoutout-expand">
|
||||||
Accessible from anywhere, anytime
|
Accessible from anywhere, anytime
|
||||||
|
@ -97,7 +98,7 @@
|
||||||
|
|
||||||
<div class="product-tour container" ng-show="user.anonymous">
|
<div class="product-tour container" ng-show="user.anonymous">
|
||||||
<div class="tour-header row">
|
<div class="tour-header row">
|
||||||
<div class="tour-shoutout-header"><i class="icon-chevron-sign-down"></i></div>
|
<div class="tour-shoutout-header"><i class="fa fa-chevron-circle-down"></i></div>
|
||||||
<div class="tour-shoutout">Take a tour of Quay</div>
|
<div class="tour-shoutout">Take a tour of Quay</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
213
static/partials/new-repo.html
Normal file
213
static/partials/new-repo.html
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
<div class="container" ng-show="user.anonymous">
|
||||||
|
<h3>Please <a href="/signin/">sign in</a></h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" ng-show="!user.anonymous && building">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" ng-show="!user.anonymous && creating">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" ng-show="!user.anonymous && uploading">
|
||||||
|
<span class="message">Uploading file {{ upload_file }}</span>
|
||||||
|
<div class="progress progress-striped active">
|
||||||
|
<div class="progress-bar" role="progressbar" aria-valuenow="{{ upload_progress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + upload_progress + '%' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container new-repo" ng-show="!user.anonymous && !creating && !uploading && !building">
|
||||||
|
<form method="post" name="newRepoForm" ng-submit="createNewRepo()">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1"></div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="section">
|
||||||
|
<div class="new-header">
|
||||||
|
<span class="repo-circle no-background" repo="repo"></span>
|
||||||
|
<span style="color: #444;"> {{user.username}}</span> <span style="color: #ccc">/</span> <span class="name-container"><input id="repoName" name="repoName" type="text" class="form-control" placeholder="Repository Name" ng-model="repo.name" required autofocus></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<strong>Description:</strong><br>
|
||||||
|
<p class="description lead editable" ng-click="editDescription()">
|
||||||
|
<span class="content" ng-bind-html-unsafe="getMarkedDown(repo.description)"></span>
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Private/public -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1"></div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="section">
|
||||||
|
<div class="repo-option">
|
||||||
|
<input type="radio" id="publicrepo" name="publicorprivate" ng-model="repo.is_public" value="1">
|
||||||
|
<i class="fa fa-unlock fa-large" title="Public Repository"></i>
|
||||||
|
|
||||||
|
<div class="option-description">
|
||||||
|
<label for="publicrepo">Public</label>
|
||||||
|
<span class="description-text">Anyone can see and pull from this repository. You choose who can push.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="repo-option">
|
||||||
|
<input type="radio" id="privaterepo" name="publicorprivate" ng-model="repo.is_public" value="0">
|
||||||
|
<i class="fa fa-lock fa-large" title="Private Repository"></i>
|
||||||
|
|
||||||
|
<div class="option-description">
|
||||||
|
<label for="privaterepo">Private</label>
|
||||||
|
<span class="description-text">You choose who can see, pull and push from/to this repository.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payment -->
|
||||||
|
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
In order to make this repository private, you’ll need to upgrade your plan from <b>{{ subscribedPlan.title }}</b> to <b>{{ planRequired.title }}</b>. This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
||||||
|
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planChanging"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Initialize repository -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1"></div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="section">
|
||||||
|
<input type="checkbox" class="cbox" id="initialize" name="initialize" ng-model="repo.initialize">
|
||||||
|
<div class="option-description">
|
||||||
|
<label for="initialize">Initialize Repository from <a href="http://www.docker.io/learn/dockerfile/" target="_new">Dockerfile</a></label>
|
||||||
|
<span class="description-text">Automatically populate your repository with a new image constructed from a Dockerfile</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="initialize-repo" ng-show="repo.initialize">
|
||||||
|
<div class="init-description">
|
||||||
|
Upload a Dockerfile or a zip file containing a Dockerfile <b>in the root directory</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input id="file-drop" class="file-drop" type="file">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1"></div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<button class="btn btn-large btn-success" type="submit" ng-disabled="newRepoForm.$invalid || (repo.is_public == '0' && planRequired)">Create Repository</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal edit for the description -->
|
||||||
|
<div class="modal fade" id="editModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Edit Repository Description</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wmd-panel">
|
||||||
|
<div id="wmd-button-bar-description"></div>
|
||||||
|
<textarea class="wmd-input" id="wmd-input-description" placeholder="Enter description">{{ repo.description }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wmd-preview-description" class="wmd-panel wmd-preview"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" ng-click="saveDescription()">Save changes</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="missingfileModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">File required</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
A file is required in order to initialize a repository.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="cannotcreateModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Cannot create repository</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
The repository could not be created.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="couldnotbuildModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Cannot initialize repository</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
The repository could not be initialized with the selected Dockerfile. Please try again later.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="couldnotsubscribeModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Cannot upgrade plan</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Your current plan could not be upgraded. Please try again.
|
||||||
|
</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 -->
|
|
@ -3,7 +3,7 @@
|
||||||
<script src="static/lib/jquery.base64.min.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="icon-spinner icon-spin icon-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" ng-show="!loading && (!repo || !permissions)">
|
<div class="container" ng-show="!loading && (!repo || !permissions)">
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
<div class="container repo repo-admin" ng-show="!loading && repo && permissions">
|
<div class="container repo repo-admin" ng-show="!loading && repo && permissions">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="icon-chevron-left"></i></a>
|
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||||
<h3>
|
<h3>
|
||||||
<span class="repo-circle no-background" repo="repo"></span> <span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
<span class="repo-circle no-background" repo="repo"></span> <span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">User Access Permissions
|
<div class="panel-heading">User Access Permissions
|
||||||
|
|
||||||
<i class="info-icon icon-info-sign" data-placement="left" data-content="Allow any number of users to read, write or administer this repository"></i>
|
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Allow any number of users to read, write or administer this repository"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<tr ng-repeat="(username, permission) in permissions">
|
<tr ng-repeat="(username, permission) in permissions">
|
||||||
<td class="user">
|
<td class="user">
|
||||||
<i class="icon-user"></i>
|
<i class="fa fa-user"></i>
|
||||||
<span>{{username}}</span>
|
<span>{{username}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="user-permissions">
|
<td class="user-permissions">
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<td>
|
<td>
|
||||||
<span class="delete-ui" tabindex="0" title="Delete Permission">
|
<span class="delete-ui" tabindex="0" title="Delete Permission">
|
||||||
<span class="delete-ui-button" ng-click="deleteRole(username)"><button class="btn btn-danger">Delete</button></span>
|
<span class="delete-ui-button" ng-click="deleteRole(username)"><button class="btn btn-danger">Delete</button></span>
|
||||||
<i class="icon-remove"></i>
|
<i class="fa fa-remove"></i>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">Access Token Permissions
|
<div class="panel-heading">Access Token Permissions
|
||||||
|
|
||||||
<i class="info-icon icon-info-sign" data-placement="left" data-content="Grant permissions to this repository by creating unique tokens that can be used without entering account passwords<br><br>To use in docker:<br><dl class='dl-horizontal'><dt>Username</dt><dd>$token</dd><dt>Password</dt><dd>(token value)</dd><dt>Email</dt><dd>(any value)</dd></dl>"></i>
|
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Grant permissions to this repository by creating unique tokens that can be used without entering account passwords<br><br>To use in docker:<br><dl class='dl-horizontal'><dt>Username</dt><dd>$token</dd><dt>Password</dt><dd>(token value)</dd><dt>Email</dt><dd>(any value)</dd></dl>"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form name="createTokenForm" ng-submit="createToken()">
|
<form name="createTokenForm" ng-submit="createToken()">
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
|
|
||||||
<tr ng-repeat="(code, token) in tokens">
|
<tr ng-repeat="(code, token) in tokens">
|
||||||
<td class="user token">
|
<td class="user token">
|
||||||
<i class="icon-key"></i>
|
<i class="fa fa-key"></i>
|
||||||
<a ng-click="showToken(token.code)">{{ token.friendlyName }}</a>
|
<a ng-click="showToken(token.code)">{{ token.friendlyName }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="user-permissions">
|
<td class="user-permissions">
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
<td>
|
<td>
|
||||||
<span class="delete-ui" tabindex="0" title="Delete Token">
|
<span class="delete-ui" tabindex="0" title="Delete Token">
|
||||||
<span class="delete-ui-button" ng-click="deleteToken(token.code)"><button class="btn btn-danger" type="button">Delete</button></span>
|
<span class="delete-ui-button" ng-click="deleteToken(token.code)"><button class="btn btn-danger" type="button">Delete</button></span>
|
||||||
<i class="icon-remove"></i>
|
<i class="fa fa-remove"></i>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
<div class="panel-heading">Repository Settings</div>
|
<div class="panel-heading">Repository Settings</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="repo-access-state" ng-show="!repo.is_public">
|
<div class="repo-access-state" ng-show="!repo.is_public">
|
||||||
<div class="state-icon"><i class="icon-lock"></i></div>
|
<div class="state-icon"><i class="fa fa-lock"></i></div>
|
||||||
|
|
||||||
This repository is currently <b>private</b>. Only users on the above access list may view and interact with it.
|
This repository is currently <b>private</b>. Only users on the above access list may view and interact with it.
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="repo-access-state" ng-show="repo.is_public">
|
<div class="repo-access-state" ng-show="repo.is_public">
|
||||||
<div class="state-icon"><i class="icon-unlock-alt"></i></div>
|
<div class="state-icon"><i class="fa fa-unlock"></i></div>
|
||||||
|
|
||||||
This repository is currently <b>public</b> and is visible to all users, and may be pulled by all users.
|
This repository is currently <b>public</b> and is visible to all users, and may be pulled by all users.
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
<h4 class="modal-title"><i class="icon-key"></i> {{ shownToken.friendlyName }}</h4>
|
<h4 class="modal-title"><i class="fa fa-key"></i> {{ shownToken.friendlyName }}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body token-dialog-body">
|
<div class="modal-body token-dialog-body">
|
||||||
<div class="alert alert-info">The docker <u>username</u> is <b>$token</b> and the <u>password</u> is the token. You may use any value for email.</div>
|
<div class="alert alert-info">The docker <u>username</u> is <b>$token</b> and the <u>password</u> is the token. You may use any value for email.</div>
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
<div class="loading" ng-show="loading">
|
<div class="loading" ng-show="loading">
|
||||||
<i class="icon-spinner icon-spin icon-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 ready-indicator" ng-show="!loading" data-status="{{ loading ? '' : 'ready' }}">
|
||||||
<div class="repo-list" ng-show="!user.anonymous">
|
<div class="repo-list" ng-show="!user.anonymous">
|
||||||
|
<a href="/new/">
|
||||||
|
<button class="btn btn-success" style="float: right">
|
||||||
|
<i class="fa fa-upload user-tool" title="Create new repository"></i>
|
||||||
|
Create Repository
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
<h3>Your Repositories</h3>
|
<h3>Your Repositories</h3>
|
||||||
<div ng-show="private_repositories.length > 0">
|
<div ng-show="private_repositories.length > 0">
|
||||||
<div class="repo-listing" ng-repeat="repository in private_repositories">
|
<div class="repo-listing" ng-repeat="repository in private_repositories">
|
||||||
|
|
|
@ -18,11 +18,11 @@
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
||||||
|
|
||||||
<span class="social-alternate">
|
<span class="social-alternate">
|
||||||
<i class="icon-circle"></i>
|
<i class="fa fa-circle"></i>
|
||||||
<span class="inner-text">OR</span>
|
<span class="inner-text">OR</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<a id='github-signin-link' href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ mixpanelDistinctIdClause }}" class="btn btn-primary btn-lg btn-block"><i class="icon-github icon-large"></i> Sign In with GitHub</a>
|
<a id='github-signin-link' href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ mixpanelDistinctIdClause }}" class="btn btn-primary btn-lg btn-block"><i class="fa fa-github fa-lg"></i> Sign In with GitHub</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
|
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="container user-admin">
|
<div class="container user-admin">
|
||||||
<div class="loading" ng-show="planLoading || planChanging">
|
<div class="loading" ng-show="planLoading || planChanging">
|
||||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" ng-show="errorMessage">
|
<div class="row" ng-show="errorMessage">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
{{ plan.title }}
|
{{ plan.title }}
|
||||||
<span class="pull-right" ng-show="subscription.plan == plan.stripeId">
|
<span class="pull-right" ng-show="subscription.plan == plan.stripeId">
|
||||||
<i class="icon-ok"></i>
|
<i class="fa fa-ok"></i>
|
||||||
Subscribed
|
Subscribed
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="loading" ng-show="updatingUser">
|
<div class="loading" ng-show="updatingUser">
|
||||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<h3>Welcome <b>{{ user.username }}</b>. Your account is fully activated!</h3>
|
<h3>Welcome <b>{{ user.username }}</b>. Your account is fully activated!</h3>
|
||||||
|
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<button class="btn btn-lg btn-primary" ng-click="browseRepos()">Browse all repositories</button>
|
<a class="btn btn-lg btn-primary" hred="/repository/">Browse all repositories</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="!user.anonymous && !user.verified">
|
<div ng-show="!user.anonymous && !user.verified">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="loading" ng-show="loading">
|
<div class="loading" ng-show="loading">
|
||||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container repo" ng-show="!loading && repo">
|
<div class="container repo" ng-show="!loading && repo">
|
||||||
|
@ -16,20 +16,19 @@
|
||||||
|
|
||||||
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings">
|
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings">
|
||||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
||||||
<i class="icon-cog icon-large"></i>
|
<i class="fa fa-cog fa-lg"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Pull command -->
|
<!-- Pull command -->
|
||||||
<div class="pull-command">
|
<div class="pull-command visible-md visible-lg" style="display: none;">
|
||||||
Get this repository:
|
<span class="pull-command-title">Pull repository:</span>
|
||||||
|
|
||||||
<div class="pull-container">
|
<div class="pull-container">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||||
<i class="icon-copy"></i>
|
<i class="fa fa-copy"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,17 +39,36 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Status boxes -->
|
||||||
|
<div class="status-boxes">
|
||||||
|
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building"
|
||||||
|
bs-popover="'static/partials/build-status-item.html'" data-placement="bottom">
|
||||||
|
<span class="title">
|
||||||
|
<i class="fa fa-spinner fa-spin"></i>
|
||||||
|
<b>Building Images</b>
|
||||||
|
</span>
|
||||||
|
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p ng-class="'description lead ' + (repo.can_write ? 'editable' : 'noteditable')" ng-click="editDescription()">
|
<div class="description">
|
||||||
<span class="content" ng-bind-html-unsafe="getMarkedDown(repo.description)"></span>
|
<p ng-class="'lead ' + (repo.can_write ? 'editable' : 'noteditable')" ng-click="editDescription()">
|
||||||
<i class="icon-edit"></i>
|
<span class="content" ng-bind-html-unsafe="getMarkedDown(repo.description)"></span>
|
||||||
</p>
|
<i class="fa fa-edit"></i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="repo-content" ng-show="!currentTag.image">
|
<!-- Empty message -->
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
|
<div class="repo-content" ng-show="!currentTag.image && repo.is_building">
|
||||||
|
<div class="empty-message">Your build is currently processing, if this takes longer than an hour, please contact <a href="mailto:support@quay.io">Quay.io support</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
@ -61,22 +79,22 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<!-- Tag dropdown -->
|
<!-- Tag dropdown -->
|
||||||
<span class="tag-dropdown dropdown" title="Tags">
|
<div class="tag-dropdown dropdown" title="Tags">
|
||||||
<i class="icon-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
|
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag.name}} <b class="caret"></b></a>
|
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag.name}} <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li ng-repeat="tag in repo.tags">
|
<li ng-repeat="tag in repo.tags">
|
||||||
<a href="javascript:void(0)" ng-click="setTag(tag.name)">{{tag.name}}</a>
|
<a href="javascript:void(0)" ng-click="setTag(tag.name)">{{tag.name}}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</div>
|
||||||
<span class="right-title">Tags</span>
|
<span class="right-title">Tags</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image history loading -->
|
<!-- Image history loading -->
|
||||||
<div ng-hide="imageHistory" style="padding: 10px; text-align: center;">
|
<div ng-hide="imageHistory" style="padding: 10px; text-align: center;">
|
||||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tree View itself -->
|
<!-- Tree View itself -->
|
||||||
|
@ -89,15 +107,15 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<!-- Image dropdown -->
|
<!-- Image dropdown -->
|
||||||
<span class="tag-dropdown dropdown" title="Images">
|
<div class="tag-dropdown dropdown" title="Images">
|
||||||
<i class="icon-archive"><span class="tag-count">{{imageHistory.length}}</span></i>
|
<i class="fa fa-archive"><span class="tag-count">{{imageHistory.length}}</span></i>
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
|
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li ng-repeat="image in imageHistory">
|
<li ng-repeat="image in imageHistory">
|
||||||
<a href="javascript:void(0)" ng-click="setImage(image)">{{image.id.substr(0, 12)}}</a>
|
<a href="javascript:void(0)" ng-click="setImage(image)">{{image.id.substr(0, 12)}}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</div>
|
||||||
<span class="right-title">Image</span>
|
<span class="right-title">Image</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -117,22 +135,22 @@
|
||||||
|
|
||||||
<!-- Image changes loading -->
|
<!-- Image changes loading -->
|
||||||
<div ng-hide="currentImageChanges">
|
<div ng-hide="currentImageChanges">
|
||||||
<i class="icon-spinner icon-spin icon-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="changes-container small-changes-container"
|
<div class="changes-container small-changes-container"
|
||||||
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
|
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
|
||||||
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
|
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
|
||||||
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added">
|
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added">
|
||||||
<i class="icon-plus-sign-alt"></i>
|
<i class="fa fa-plus-square"></i>
|
||||||
<b>{{currentImageChanges.added.length}}</b>
|
<b>{{currentImageChanges.added.length}}</b>
|
||||||
</span>
|
</span>
|
||||||
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" title="Files Removed">
|
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" title="Files Removed">
|
||||||
<i class="icon-minus-sign-alt"></i>
|
<i class="fa fa-minus-square"></i>
|
||||||
<b>{{currentImageChanges.removed.length}}</b>
|
<b>{{currentImageChanges.removed.length}}</b>
|
||||||
</span>
|
</span>
|
||||||
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" title="Files Changed">
|
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" title="Files Changed">
|
||||||
<i class="icon-edit-sign"></i>
|
<i class="fa fa-pencil-square"></i>
|
||||||
<b>{{currentImageChanges.changed.length}}</b>
|
<b>{{currentImageChanges.changed.length}}</b>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -140,15 +158,15 @@
|
||||||
<div id="collapseChanges" class="panel-collapse collapse in">
|
<div id="collapseChanges" class="panel-collapse collapse in">
|
||||||
<div class="well well-sm">
|
<div class="well well-sm">
|
||||||
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:5">
|
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:5">
|
||||||
<i class="icon-plus-sign-alt"></i>
|
<i class="fa fa-plus-square"></i>
|
||||||
<span title="{{file}}">{{file}}</span>
|
<span title="{{file}}">{{file}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:5">
|
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:5">
|
||||||
<i class="icon-minus-sign-alt"></i>
|
<i class="fa fa-minus-square"></i>
|
||||||
<span title="{{file}}">{{file}}</span>
|
<span title="{{file}}">{{file}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:5">
|
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:5">
|
||||||
<i class="icon-edit-sign"></i>
|
<i class="fa fa-pencil-square"></i>
|
||||||
<span title="{{file}}">{{file}}</span>
|
<span title="{{file}}">{{file}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<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.">
|
<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/3.2.1/css/font-awesome.min.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">
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
|
||||||
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
|
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
|
||||||
|
@ -43,6 +43,7 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
<script src="static/lib/angular-strap.min.js"></script>
|
||||||
<script src="static/lib/angulartics.js"></script>
|
<script src="static/lib/angulartics.js"></script>
|
||||||
<script src="static/lib/angulartics-mixpanel.js"></script>
|
<script src="static/lib/angulartics-mixpanel.js"></script>
|
||||||
|
|
||||||
|
@ -50,6 +51,10 @@
|
||||||
|
|
||||||
<script src="static/lib/typeahead.min.js"></script>
|
<script src="static/lib/typeahead.min.js"></script>
|
||||||
|
|
||||||
|
<script src="static/lib/pagedown/Markdown.Converter.js"></script>
|
||||||
|
<script src="static/lib/pagedown/Markdown.Editor.js"></script>
|
||||||
|
<script src="static/lib/pagedown/Markdown.Sanitizer.js"></script>
|
||||||
|
|
||||||
{% block added_dependencies %}
|
{% block added_dependencies %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -24,14 +24,10 @@
|
||||||
|
|
||||||
<script src="static/lib/jquery.overscroll.min.js"></script>
|
<script src="static/lib/jquery.overscroll.min.js"></script>
|
||||||
|
|
||||||
<script src="static/lib/pagedown/Markdown.Converter.js"></script>
|
|
||||||
<script src="static/lib/pagedown/Markdown.Editor.js"></script>
|
|
||||||
<script src="static/lib/pagedown/Markdown.Sanitizer.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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body_content %}
|
{% block body_content %}
|
||||||
<div ng-view></div>
|
<div ng-view></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Binary file not shown.
|
@ -7,10 +7,10 @@ import argparse
|
||||||
from apscheduler.scheduler import Scheduler
|
from apscheduler.scheduler import Scheduler
|
||||||
|
|
||||||
from data.queue import image_diff_queue
|
from data.queue import image_diff_queue
|
||||||
|
from data.database import db as db_connection
|
||||||
from endpoints.registry import process_image_changes
|
from endpoints.registry import process_image_changes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
root_logger = logging.getLogger('')
|
root_logger = logging.getLogger('')
|
||||||
root_logger.setLevel(logging.DEBUG)
|
root_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
@ -38,6 +38,10 @@ def process_work_items():
|
||||||
|
|
||||||
logger.debug('No more work.')
|
logger.debug('No more work.')
|
||||||
|
|
||||||
|
if not db_connection.is_closed():
|
||||||
|
logger.debug('Closing thread db connection.')
|
||||||
|
db_connection.close()
|
||||||
|
|
||||||
|
|
||||||
def start_worker():
|
def start_worker():
|
||||||
logger.debug("Scheduling worker.")
|
logger.debug("Scheduling worker.")
|
||||||
|
@ -59,11 +63,6 @@ parser.add_argument('--log', default='diffsworker.log',
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
# if not args.D:
|
|
||||||
# else:
|
|
||||||
# logging.basicConfig(format=FORMAT, level=logging.DEBUG)
|
|
||||||
# start_worker(args)
|
|
||||||
|
|
||||||
if args.D:
|
if args.D:
|
||||||
handler = logging.FileHandler(args.log)
|
handler = logging.FileHandler(args.log)
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
|
|
279
workers/dockerfilebuild.py
Normal file
279
workers/dockerfilebuild.py
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import daemon
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
import digitalocean
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
from apscheduler.scheduler import Scheduler
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
from base64 import b64encode
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
from data.queue import dockerfile_build_queue
|
||||||
|
from data.userfiles import UserRequestFiles
|
||||||
|
from data import model
|
||||||
|
from data.database import db as db_connection
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
|
||||||
|
root_logger = logging.getLogger('')
|
||||||
|
root_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
FORMAT = '%(asctime)-15s - %(levelname)s - %(pathname)s - %(funcName)s - %(message)s'
|
||||||
|
formatter = logging.Formatter(FORMAT)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BUILD_SERVER_CMD = ('docker run -d -lxc-conf="lxc.aa_profile=unconfined" ' +
|
||||||
|
'-privileged -e \'RESOURCE_URL=%s\' -e \'TAG=%s\' ' +
|
||||||
|
'-e \'TOKEN=%s\' quay.io/quay/buildserver')
|
||||||
|
|
||||||
|
|
||||||
|
def retry_command(to_call, args=[], kwargs={}, retries=5, period=5):
|
||||||
|
try:
|
||||||
|
return to_call(*args, **kwargs)
|
||||||
|
except Exception as ex:
|
||||||
|
if retries:
|
||||||
|
logger.debug('Retrying command after %ss' % period)
|
||||||
|
time.sleep(period)
|
||||||
|
return retry_command(to_call, args, kwargs, retries-1, period)
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
|
||||||
|
def get_status(url):
|
||||||
|
return retry_command(requests.get, [url]).json()['status']
|
||||||
|
|
||||||
|
|
||||||
|
def babysit_builder(request):
|
||||||
|
""" Spin up a build node and ask it to build our job. Retryable errors
|
||||||
|
should return False, while fatal errors should return True.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.debug('Starting work item: %s' % request)
|
||||||
|
repository_build = model.get_repository_build(request['build_id'])
|
||||||
|
logger.debug('Request details: %s' % repository_build)
|
||||||
|
|
||||||
|
# Initialize digital ocean API
|
||||||
|
do_client_id = app.config['DO_CLIENT_ID']
|
||||||
|
do_api_key = app.config['DO_CLIENT_SECRET']
|
||||||
|
manager = digitalocean.Manager(client_id=do_client_id, api_key=do_api_key)
|
||||||
|
|
||||||
|
# check if there is already a DO node for this build, if so clean it up
|
||||||
|
old_id = repository_build.build_node_id
|
||||||
|
if old_id:
|
||||||
|
logger.debug('Cleaning up old DO node: %s' % old_id)
|
||||||
|
old_droplet = digitalocean.Droplet(id=old_id, client_id=do_client_id,
|
||||||
|
api_key=do_api_key)
|
||||||
|
retry_command(old_droplet.destroy)
|
||||||
|
|
||||||
|
# Pick the region for the new droplet
|
||||||
|
allowed_regions = app.config['DO_ALLOWED_REGIONS']
|
||||||
|
regions = retry_command(manager.get_all_regions)
|
||||||
|
available_regions = {region.id for region in regions}
|
||||||
|
regions = available_regions.intersection(allowed_regions)
|
||||||
|
if not regions:
|
||||||
|
logger.error('No droplets in our allowed regtions, available: %s' %
|
||||||
|
available_regions)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# start the DO node
|
||||||
|
name = 'dockerfile-build-%s' % repository_build.id
|
||||||
|
logger.debug('Starting DO node: %s' % name)
|
||||||
|
droplet = digitalocean.Droplet(client_id=do_client_id,
|
||||||
|
api_key=do_api_key,
|
||||||
|
name=name,
|
||||||
|
region_id=regions.pop(),
|
||||||
|
image_id=1004145, # Docker on 13.04
|
||||||
|
size_id=66, # 512MB,
|
||||||
|
backup_active=False)
|
||||||
|
retry_command(droplet.create, [],
|
||||||
|
{'ssh_key_ids': [app.config['DO_SSH_KEY_ID']]})
|
||||||
|
repository_build.build_node_id = droplet.id
|
||||||
|
repository_build.phase = 'starting'
|
||||||
|
repository_build.save()
|
||||||
|
|
||||||
|
logger.debug('Waiting for DO node to be available.')
|
||||||
|
|
||||||
|
startup = retry_command(droplet.get_events)[0]
|
||||||
|
while not startup.percentage or int(startup.percentage) != 100:
|
||||||
|
logger.debug('Droplet startup percentage: %s' % startup.percentage)
|
||||||
|
time.sleep(5)
|
||||||
|
retry_command(startup.load)
|
||||||
|
|
||||||
|
retry_command(droplet.load)
|
||||||
|
logger.debug('Droplet started at ip address: %s' % droplet.ip_address)
|
||||||
|
|
||||||
|
# connect to it with ssh
|
||||||
|
repository_build.phase = 'initializing'
|
||||||
|
repository_build.save()
|
||||||
|
|
||||||
|
# We wait until here to import paramiko because otherwise it doesn't work
|
||||||
|
# under the daemon context.
|
||||||
|
import paramiko
|
||||||
|
ssh_client = paramiko.SSHClient()
|
||||||
|
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
logger.debug('Connecting to droplet through ssh at ip: %s' %
|
||||||
|
droplet.ip_address)
|
||||||
|
retry_command(ssh_client.connect, [droplet.ip_address, 22, 'root'],
|
||||||
|
{'look_for_keys': False, 'timeout': 10.0,
|
||||||
|
'key_filename': app.config['DO_SSH_PRIVATE_KEY_FILENAME']})
|
||||||
|
|
||||||
|
# Load the node with the pull token
|
||||||
|
token = app.config['BUILD_NODE_PULL_TOKEN']
|
||||||
|
basicauth = b64encode('%s:%s' % ('$token', token))
|
||||||
|
auth_object = {
|
||||||
|
'https://quay.io/v1/': {
|
||||||
|
'auth': basicauth,
|
||||||
|
'email': '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
create_auth_cmd = 'echo \'%s\' > .dockercfg' % json.dumps(auth_object)
|
||||||
|
ssh_client.exec_command(create_auth_cmd)
|
||||||
|
|
||||||
|
# Pull and run the buildserver
|
||||||
|
pull_cmd = 'docker pull quay.io/quay/buildserver'
|
||||||
|
_, stdout, _ = ssh_client.exec_command(pull_cmd)
|
||||||
|
pull_status = stdout.channel.recv_exit_status()
|
||||||
|
|
||||||
|
if pull_status != 0:
|
||||||
|
logger.error('Pull command failed for host: %s' % droplet.ip_address)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.debug('Pull status was: %s' % pull_status)
|
||||||
|
|
||||||
|
# Remove the credentials we used to pull so crafty users cant steal them
|
||||||
|
remove_auth_cmd = 'rm .dockercfg'
|
||||||
|
ssh_client.exec_command(remove_auth_cmd)
|
||||||
|
|
||||||
|
# Prepare the signed resource url the build node can fetch the job from
|
||||||
|
user_files = UserRequestFiles(app.config['AWS_ACCESS_KEY'],
|
||||||
|
app.config['AWS_SECRET_KEY'],
|
||||||
|
app.config['REGISTRY_S3_BUCKET'])
|
||||||
|
resource_url = user_files.get_file_url(repository_build.resource_key)
|
||||||
|
|
||||||
|
# Start the build server
|
||||||
|
start_cmd = BUILD_SERVER_CMD % (resource_url, repository_build.tag,
|
||||||
|
repository_build.access_token.code)
|
||||||
|
logger.debug('Sending build server request with command: %s' % start_cmd)
|
||||||
|
ssh_client.exec_command(start_cmd)
|
||||||
|
|
||||||
|
status_endpoint = 'http://%s:5002/build/' % droplet.ip_address
|
||||||
|
# wait for the server to be ready
|
||||||
|
logger.debug('Waiting for buildserver to be ready')
|
||||||
|
retry_command(requests.get, [status_endpoint])
|
||||||
|
|
||||||
|
# wait for the job to be complete
|
||||||
|
repository_build.phase = 'building'
|
||||||
|
repository_build.status_url = status_endpoint
|
||||||
|
repository_build.save()
|
||||||
|
|
||||||
|
logger.debug('Waiting for job to be complete')
|
||||||
|
status = get_status(status_endpoint)
|
||||||
|
while status != 'error' and status != 'complete':
|
||||||
|
logger.debug('Job status is: %s' % status)
|
||||||
|
time.sleep(5)
|
||||||
|
status = get_status(status_endpoint)
|
||||||
|
|
||||||
|
logger.debug('Job complete with status: %s' % status)
|
||||||
|
if status == 'error':
|
||||||
|
error_message = requests.get(status_endpoint).json()['message']
|
||||||
|
logger.warning('Job error: %s' % error_message)
|
||||||
|
repository_build.phase = 'error'
|
||||||
|
else:
|
||||||
|
repository_build.phase = 'complete'
|
||||||
|
|
||||||
|
# clean up the DO node
|
||||||
|
logger.debug('Cleaning up DO node.')
|
||||||
|
retry_command(droplet.destroy)
|
||||||
|
|
||||||
|
repository_build.status_url = None
|
||||||
|
repository_build.build_node_id = None;
|
||||||
|
repository_build.save()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as outer_ex:
|
||||||
|
# We don't really know what these are, but they are probably retryable
|
||||||
|
logger.exception('Exception processing job: %s' % outer_ex.message)
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if not db_connection.is_closed():
|
||||||
|
logger.debug('Closing thread db connection.')
|
||||||
|
db_connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def process_work_items(pool):
|
||||||
|
logger.debug('Getting work item from queue.')
|
||||||
|
|
||||||
|
item = dockerfile_build_queue.get(processing_time=60*60) # allow 1 hr
|
||||||
|
|
||||||
|
while item:
|
||||||
|
logger.debug('Queue gave us some work: %s' % item.body)
|
||||||
|
|
||||||
|
request = json.loads(item.body)
|
||||||
|
|
||||||
|
def build_callback(item):
|
||||||
|
local_item = item
|
||||||
|
def complete_callback(completed):
|
||||||
|
if completed:
|
||||||
|
dockerfile_build_queue.complete(local_item)
|
||||||
|
else:
|
||||||
|
# We have a retryable error, add the job back to the queue
|
||||||
|
dockerfile_build_queue.incomplete(local_item)
|
||||||
|
|
||||||
|
return complete_callback
|
||||||
|
|
||||||
|
logger.debug('Sending work item to thread pool: %s' % pool)
|
||||||
|
pool.apply_async(babysit_builder, [request],
|
||||||
|
callback=build_callback(item))
|
||||||
|
|
||||||
|
item = dockerfile_build_queue.get()
|
||||||
|
|
||||||
|
logger.debug('No more work.')
|
||||||
|
|
||||||
|
if not db_connection.is_closed():
|
||||||
|
logger.debug('Closing thread db connection.')
|
||||||
|
db_connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def start_worker():
|
||||||
|
pool = ThreadPool(3)
|
||||||
|
logger.debug('Scheduling worker.')
|
||||||
|
|
||||||
|
sched = Scheduler()
|
||||||
|
sched.start()
|
||||||
|
|
||||||
|
sched.add_interval_job(process_work_items, args=[pool], seconds=30)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
time.sleep(60 * 60 * 24) # sleep one day, basically forever
|
||||||
|
|
||||||
|
|
||||||
|
desc = 'Worker daemon to monitor dockerfile build'
|
||||||
|
parser = argparse.ArgumentParser(description=desc)
|
||||||
|
parser.add_argument('-D', action='store_true', default=False,
|
||||||
|
help='Run the worker in daemon mode.')
|
||||||
|
parser.add_argument('--log', default='dockerfilebuild.log',
|
||||||
|
help='Specify the log file for the worker as a daemon.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if args.D:
|
||||||
|
handler = logging.FileHandler(args.log)
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(handler)
|
||||||
|
with daemon.DaemonContext(files_preserve=[handler.stream],
|
||||||
|
working_directory=os.getcwd()):
|
||||||
|
start_worker()
|
||||||
|
|
||||||
|
else:
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(handler)
|
||||||
|
start_worker()
|
Reference in a new issue