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:
|
||||
|
||||
```
|
||||
sudo apt-get install software-properties-common
|
||||
sudo apt-add-repository -y ppa:nginx/stable
|
||||
sudo apt-get update
|
||||
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.dockerfilebuild -D
|
||||
```
|
||||
|
||||
bouncing the servers:
|
||||
|
|
|
@ -2,16 +2,25 @@ import logging
|
|||
|
||||
from app import app as application
|
||||
|
||||
|
||||
logging.basicConfig(**application.config['LOGGING_CONFIG'])
|
||||
|
||||
|
||||
import endpoints.index
|
||||
import endpoints.api
|
||||
import endpoints.web
|
||||
import endpoints.tags
|
||||
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
|
||||
application.debug = True
|
||||
|
||||
logging.basicConfig(**application.config['LOGGING_CONFIG'])
|
||||
|
||||
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
|
||||
|
||||
|
||||
class S3Storage(object):
|
||||
class AWSCredentials(object):
|
||||
AWS_ACCESS_KEY = 'AKIAJWZWUIS24TWSMWRA'
|
||||
AWS_SECRET_KEY = 'EllGwP+noVvzmsUGQJO1qOMk3vm10Vg+UE6xmmpw'
|
||||
REGISTRY_S3_BUCKET = 'quay-registry'
|
||||
|
||||
|
||||
class S3Storage(AWSCredentials):
|
||||
STORAGE_KIND = 's3'
|
||||
|
||||
|
||||
|
@ -85,21 +88,34 @@ class GitHubProdConfig(GitHubTestConfig):
|
|||
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,
|
||||
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig):
|
||||
REGISTRY_SERVER = 'localhost:5000'
|
||||
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig,
|
||||
DigitalOceanConfig, AWSCredentials, BuildNodeConfig):
|
||||
LOGGING_CONFIG = {
|
||||
'level': logging.DEBUG,
|
||||
'format': LOG_FORMAT
|
||||
}
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
POPULATE_DB_TEST_DATA = True
|
||||
INCLUDE_TEST_ENDPOINTS = True
|
||||
|
||||
|
||||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelTestConfig,
|
||||
GitHubProdConfig):
|
||||
REGISTRY_SERVER = 'localhost:5000'
|
||||
GitHubProdConfig, DigitalOceanConfig,
|
||||
BuildNodeConfig):
|
||||
LOGGING_CONFIG = {
|
||||
'level': logging.DEBUG,
|
||||
'format': LOG_FORMAT
|
||||
|
@ -109,8 +125,7 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
|||
|
||||
class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig, MixpanelProdConfig,
|
||||
GitHubProdConfig):
|
||||
REGISTRY_SERVER = 'quay.io'
|
||||
GitHubProdConfig, DigitalOceanConfig, BuildNodeConfig):
|
||||
LOGGING_CONFIG = {
|
||||
'stream': sys.stderr,
|
||||
'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):
|
||||
queue_name = CharField(index=True)
|
||||
body = TextField()
|
||||
|
@ -162,7 +172,7 @@ def initialize_db():
|
|||
create_model_tables([User, Repository, Image, AccessToken, Role,
|
||||
RepositoryPermission, Visibility, RepositoryTag,
|
||||
EmailConfirmation, FederatedLogin, LoginService,
|
||||
QueueItem])
|
||||
QueueItem, RepositoryBuild])
|
||||
Role.create(name='admin')
|
||||
Role.create(name='write')
|
||||
Role.create(name='read')
|
||||
|
|
|
@ -30,6 +30,10 @@ class InvalidTokenException(DataModelException):
|
|||
pass
|
||||
|
||||
|
||||
class InvalidRepositoryBuildException(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
def create_user(username, password, email):
|
||||
if not validate_email(email):
|
||||
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
||||
|
@ -283,8 +287,8 @@ def set_repository_visibility(repo, visibility):
|
|||
repo.save()
|
||||
|
||||
|
||||
def create_repository(namespace, name, owner):
|
||||
private = Visibility.get(name='private')
|
||||
def create_repository(namespace, name, owner, visibility='private'):
|
||||
private = Visibility.get(name=visibility)
|
||||
repo = Repository.create(namespace=namespace, name=name,
|
||||
visibility=private)
|
||||
admin = Role.get(name='admin')
|
||||
|
@ -548,3 +552,28 @@ def load_token_data(code):
|
|||
return fetched[0]
|
||||
else:
|
||||
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):
|
||||
completed_item.delete_instance()
|
||||
|
||||
def incomplete(self, incomplete_item):
|
||||
incomplete_item.available = True
|
||||
incomplete_item.save()
|
||||
|
||||
|
||||
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 stripe
|
||||
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.principal import identity_changed, AnonymousIdentity
|
||||
from functools import wraps
|
||||
|
@ -11,6 +14,8 @@ from collections import defaultdict
|
|||
import storage
|
||||
|
||||
from data import model
|
||||
from data.userfiles import UserRequestFiles
|
||||
from data.queue import dockerfile_build_queue
|
||||
from app import app
|
||||
from util.email import send_confirmation_email, send_recovery_email
|
||||
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
|
||||
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'])
|
||||
|
@ -323,6 +347,9 @@ def get_repo_api(namespace, repository):
|
|||
tag_dict = {tag.name: tag_view(tag) for tag in tags}
|
||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
||||
active_builds = model.list_repository_builds(namespace, repository,
|
||||
include_inactive=False)
|
||||
|
||||
return jsonify({
|
||||
'namespace': namespace,
|
||||
'name': repository,
|
||||
|
@ -330,13 +357,83 @@ def get_repo_api(namespace, repository):
|
|||
'tags': tag_dict,
|
||||
'can_write': can_write,
|
||||
'can_admin': can_admin,
|
||||
'is_public': is_public
|
||||
'is_public': is_public,
|
||||
'is_building': len(active_builds) > 0,
|
||||
})
|
||||
|
||||
abort(404) # Not fount
|
||||
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):
|
||||
return {
|
||||
'role': repo_perm_obj.role.name
|
||||
|
|
|
@ -2,6 +2,7 @@ import json
|
|||
import urllib
|
||||
import json
|
||||
import logging
|
||||
import urlparse
|
||||
|
||||
from flask import request, make_response, jsonify, abort
|
||||
from functools import wraps
|
||||
|
@ -25,7 +26,9 @@ def generate_headers(role='read'):
|
|||
def wrapper(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', '')
|
||||
|
||||
|
|
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('')
|
||||
|
||||
|
||||
@app.route('/new/')
|
||||
def new():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/repository/')
|
||||
def repository():
|
||||
return index('')
|
||||
|
|
18
initdb.py
18
initdb.py
|
@ -5,6 +5,7 @@ import os
|
|||
import hashlib
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from flask import url_for
|
||||
|
||||
import storage
|
||||
|
||||
|
@ -95,6 +96,8 @@ def __generate_repository(user, name, description, is_public, permissions,
|
|||
|
||||
create_subtree(repo, structure, None)
|
||||
|
||||
return repo
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
initialize_db()
|
||||
|
@ -141,6 +144,15 @@ if __name__ == '__main__':
|
|||
'Shared repository, another user can write.', False,
|
||||
[(new_user_2, 'write')], (5, [], 'latest'))
|
||||
|
||||
__generate_repository(new_user_1, 'empty',
|
||||
'Empty repository with no images or tags.', False,
|
||||
[], (0, [], None))
|
||||
building = __generate_repository(new_user_1, 'building',
|
||||
'Empty repository which is building.',
|
||||
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 {
|
||||
types_hash_max_size 2048;
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
default_type application/octet-stream;
|
||||
|
@ -36,8 +37,8 @@ http {
|
|||
keepalive_timeout 5;
|
||||
|
||||
ssl on;
|
||||
ssl_certificate /home/ubuntu/quay/certs/unified.cert;
|
||||
ssl_certificate_key /home/ubuntu/quay/certs/quay.key;
|
||||
ssl_certificate ./certs/quay-unified.cert;
|
||||
ssl_certificate_key ./certs/quay.key;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_protocols SSLv3 TLSv1;
|
||||
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
||||
|
|
|
@ -14,4 +14,6 @@ mixpanel-py
|
|||
beautifulsoup4
|
||||
marisa-trie
|
||||
apscheduler
|
||||
python-daemon
|
||||
python-daemon
|
||||
paramiko
|
||||
python-digitalocean
|
|
@ -12,6 +12,7 @@ beautifulsoup4==4.3.2
|
|||
blinker==1.3
|
||||
boto==2.15.0
|
||||
distribute==0.6.34
|
||||
ecdsa==0.10
|
||||
eventlet==0.14.0
|
||||
greenlet==0.4.1
|
||||
gunicorn==18.0
|
||||
|
@ -19,10 +20,13 @@ itsdangerous==0.23
|
|||
lockfile==0.9.1
|
||||
marisa-trie==0.5.1
|
||||
mixpanel-py==3.0.0
|
||||
paramiko==1.12.0
|
||||
peewee==2.1.4
|
||||
py-bcrypt==0.4
|
||||
pycrypto==2.6.1
|
||||
python-daemon==1.6
|
||||
python-dateutil==2.1
|
||||
python-digitalocean==0.5
|
||||
requests==2.0.0
|
||||
six==1.4.1
|
||||
stripe==1.9.8
|
||||
|
|
|
@ -36,13 +36,57 @@
|
|||
100% { -webkit-transform: scale(1); }
|
||||
}
|
||||
|
||||
|
||||
@keyframes scaleup {
|
||||
0% { transform: scale(0); }
|
||||
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 {
|
||||
position: relative;
|
||||
|
@ -57,15 +101,23 @@
|
|||
|
||||
.repo-circle.no-background {
|
||||
background: transparent;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.repo-circle .icon-lock {
|
||||
font-size: 50%;
|
||||
.repo-circle .fa-hdd {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.repo-circle.no-background .fa-hdd {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.repo-circle .fa-lock {
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
right: 0px;
|
||||
bottom: -2px;
|
||||
right: -4px;
|
||||
background: rgb(253, 191, 191);
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
|
@ -73,11 +125,11 @@
|
|||
text-align: center;
|
||||
height: 20px;
|
||||
line-height: 21px;
|
||||
font-size: 12px;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.repo-circle.no-background .icon-lock {
|
||||
bottom: -4px;
|
||||
.repo-circle.no-background .fa-lock {
|
||||
bottom: -2px;
|
||||
right: -6px;
|
||||
color: #444;
|
||||
}
|
||||
|
@ -117,6 +169,85 @@
|
|||
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 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
@ -532,6 +663,7 @@ p.editable:hover i {
|
|||
}
|
||||
|
||||
.repo .description {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
|
@ -561,22 +693,70 @@ p.editable:hover i {
|
|||
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 {
|
||||
float: right;
|
||||
display: inline-block;
|
||||
font-size: 1.2em;
|
||||
font-size: 0.8em;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
margin-top: 30px;
|
||||
margin-right: 26px;
|
||||
}
|
||||
|
||||
.repo .pull-command .pull-container {
|
||||
.repo .pull-container {
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.repo .pull-command input {
|
||||
.repo .pull-container input {
|
||||
cursor: default;
|
||||
background: white;
|
||||
color: #666;
|
||||
|
@ -702,7 +882,6 @@ p.editable:hover i {
|
|||
}
|
||||
|
||||
.repo-listing i {
|
||||
font-size: 1.5em;
|
||||
color: #999;
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
|
@ -742,8 +921,22 @@ p.editable:hover i {
|
|||
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 {
|
||||
|
@ -793,15 +986,15 @@ p.editable:hover i {
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.repo .changes-container i.icon-plus-sign-alt {
|
||||
.repo .changes-container i.fa-plus-square {
|
||||
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);
|
||||
}
|
||||
|
||||
.repo .changes-container i.icon-edit-sign {
|
||||
.repo .changes-container i.fa-pencil-square {
|
||||
color: rgb(73, 100, 209);
|
||||
}
|
||||
|
||||
|
@ -929,11 +1122,11 @@ p.editable:hover i {
|
|||
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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
|
@ -1105,6 +1298,7 @@ p.editable:hover i {
|
|||
border: 1px solid #eee;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
/* 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="icon-hdd icon-large"></i>
|
||||
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: visible' }}" title="Private Repository"></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.
|
||||
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) {
|
||||
var userResponse = {
|
||||
verified: false,
|
||||
|
@ -54,7 +54,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
|||
return keyService;
|
||||
}]);
|
||||
|
||||
$provide.factory('PlanService', [function() {
|
||||
$provide.factory('PlanService', ['Restangular', 'KeyService', function(Restangular, KeyService) {
|
||||
var plans = [
|
||||
{
|
||||
title: 'Open Source',
|
||||
|
@ -96,11 +96,54 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
|||
|
||||
planService.planList = function() {
|
||||
return plans;
|
||||
}
|
||||
};
|
||||
|
||||
planService.getPlan = function(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;
|
||||
}]);
|
||||
|
@ -155,6 +198,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
|||
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('/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}).
|
||||
|
||||
|
@ -176,7 +220,73 @@ quayApp.directive('repoCircle', function () {
|
|||
'repo': '=repo'
|
||||
},
|
||||
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;
|
||||
|
|
|
@ -92,7 +92,7 @@ function HeaderCtrl($scope, $location, UserService, Restangular) {
|
|||
},
|
||||
template: function (datum) {
|
||||
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>'
|
||||
if (datum.repo.description) {
|
||||
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();
|
||||
|
||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||
|
@ -236,10 +236,6 @@ function LandingCtrl($scope, $timeout, Restangular, UserService, KeyService) {
|
|||
return getMarkedDown(getFirstTextLine(commentString));
|
||||
};
|
||||
|
||||
$scope.browseRepos = function() {
|
||||
document.location = '/repository/';
|
||||
};
|
||||
|
||||
$scope.register = function() {
|
||||
$('.form-signup').popover('hide');
|
||||
$scope.registering = true;
|
||||
|
@ -346,12 +342,83 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
|||
}
|
||||
});
|
||||
|
||||
var listImages = function() {
|
||||
if ($scope.imageHistory) { return; }
|
||||
var fetchRepository = function() {
|
||||
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/');
|
||||
imageFetch.get().then(function(resp) {
|
||||
$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.getCommentFirstLine, $scope.getTimeSince);
|
||||
|
||||
|
@ -363,7 +430,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
|||
}
|
||||
|
||||
$($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.$apply(function() { $scope.setImage(e.image); });
|
||||
|
@ -378,7 +445,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
|||
changesFetch.get().then(function(changeInfo) {
|
||||
$scope.currentImageChanges = changeInfo;
|
||||
}, function() {
|
||||
$scope.currentImageChanges = {};
|
||||
$scope.currentImageChanges = {'added': [], 'removed': [], 'changed': []};
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -441,20 +508,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope, $location, $tim
|
|||
$scope.loading = true;
|
||||
|
||||
// Fetch the repo.
|
||||
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;
|
||||
}, function() {
|
||||
$scope.repo = null;
|
||||
$scope.loading = false;
|
||||
$rootScope.title = 'Unknown Repository';
|
||||
});
|
||||
fetchRepository();
|
||||
|
||||
// Fetch the image history.
|
||||
listImages();
|
||||
|
@ -492,7 +546,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
},
|
||||
template: function (datum) {
|
||||
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 += '</div>'
|
||||
return template;
|
||||
|
@ -702,6 +756,8 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
|||
|
||||
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.';
|
||||
} else {
|
||||
$scope.errorMessage = null;
|
||||
}
|
||||
|
||||
$scope.planLoading = false;
|
||||
|
@ -710,7 +766,7 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
|||
mixpanel.people.set({
|
||||
'plan': sub.plan
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.planLoading = true;
|
||||
var getSubscription = Restangular.one('user/plan');
|
||||
|
@ -721,36 +777,16 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
|||
|
||||
$scope.planChanging = false;
|
||||
$scope.subscribe = function(planId) {
|
||||
var submitToken = function(token) {
|
||||
$scope.$apply(function() {
|
||||
mixpanel.track('plan_subscribe');
|
||||
|
||||
$scope.planChanging = true;
|
||||
$scope.errorMessage = undefined;
|
||||
|
||||
var subscriptionDetails = {
|
||||
token: token.id,
|
||||
plan: planId,
|
||||
};
|
||||
|
||||
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
|
||||
PlanService.showSubscribeDialog($scope, planId, function() {
|
||||
// Subscribing.
|
||||
$scope.planChanging = true;
|
||||
}, function(plan) {
|
||||
// Subscribed.
|
||||
subscribedToPlan(plan);
|
||||
}, function() {
|
||||
// Failure.
|
||||
$scope.errorMessage = 'Unable to subscribe.';
|
||||
$scope.planChanging = false;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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.user = currentUser;
|
||||
}, true);
|
||||
|
||||
$scope.browseRepos = function() {
|
||||
document.location = '/repository/';
|
||||
$scope.repo = {
|
||||
'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)
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -1039,17 +1058,17 @@ ImageFileChangeTree.prototype.update_ = function(source) {
|
|||
node.select('.node-icon')
|
||||
.html(function(d) {
|
||||
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>';
|
||||
}
|
||||
|
||||
var icon = {
|
||||
'added': 'plus-sign-alt',
|
||||
'removed': 'minus-sign-alt',
|
||||
'changed': 'edit-sign'
|
||||
'added': 'plus-square',
|
||||
'removed': 'minus-square',
|
||||
'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.
|
||||
|
|
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, $) {
|
||||
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() {
|
||||
$('[data-screenshot-url]').each(function(index, element) {
|
||||
|
|
|
@ -1359,42 +1359,42 @@
|
|||
}
|
||||
|
||||
group1 = makeGroup(1);
|
||||
buttons.bold = makeButton("wmd-bold-button", "Bold - Ctrl+B", "icon-bold", bindCommand("doBold"), group1);
|
||||
buttons.italic = makeButton("wmd-italic-button", "Italic - Ctrl+I", "icon-italic", bindCommand("doItalic"), 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", "fa fa-italic", bindCommand("doItalic"), group1);
|
||||
|
||||
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);
|
||||
}), group2);
|
||||
*/
|
||||
buttons.quote = makeButton("wmd-quote-button", "Blockquote - Ctrl+Q", "icon-quote-left", bindCommand("doBlockquote"), group2);
|
||||
buttons.code = makeButton("wmd-code-button", "Code Sample - Ctrl+K", "icon-code", bindCommand("doCode"), 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", "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);
|
||||
}), group2);
|
||||
*/
|
||||
|
||||
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);
|
||||
}), 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);
|
||||
}), group3);
|
||||
buttons.heading = makeButton("wmd-heading-button", "Heading - Ctrl+H", "icon-tasks", bindCommand("doHeading"), group3);
|
||||
buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule - Ctrl+R", "icon-minus", bindCommand("doHorizontalRule"), 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", "fa fa-minus", bindCommand("doHorizontalRule"), group3);
|
||||
|
||||
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(); };
|
||||
|
||||
var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
|
||||
"Redo - Ctrl+Y" :
|
||||
"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(); };
|
||||
|
||||
if (helpOptions) {
|
||||
|
@ -1402,7 +1402,7 @@
|
|||
group5.className = group5.className + " pull-right";
|
||||
var helpButton = document.createElement("button");
|
||||
var helpButtonImage = document.createElement("i");
|
||||
helpButtonImage.className = "icon-question-sign";
|
||||
helpButtonImage.className = "fa fa-question-sign";
|
||||
helpButton.appendChild(helpButtonImage);
|
||||
helpButton.className = "btn";
|
||||
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">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="fa-bar"></span>
|
||||
<span class="fa-bar"></span>
|
||||
<span class="fa-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/" target="{{ appLinkTarget() }}">
|
||||
<img src="/static/img/quay-logo.png">
|
||||
|
@ -27,9 +27,11 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<li class="dropdown" ng-switch-when="false">
|
||||
<!--<button type="button" class="btn btn-default navbar-btn">Sign in</button>-->
|
||||
<span class="navbar-left user-tools" ng-show="!user.anonymous">
|
||||
<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">
|
||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
||||
{{ user.username }}
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
</div>
|
||||
|
||||
<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 class="container repo repo-image-view" ng-show="!loading && image">
|
||||
<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>
|
||||
<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: #ccc">/</span>
|
||||
<span style="color: #666;">{{repo.name}}</span>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<div class="input-group">
|
||||
<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">
|
||||
<i class="icon-copy"></i>
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -75,7 +75,7 @@
|
|||
No matching changes
|
||||
</div>
|
||||
<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 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>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<div ng-show="!user.anonymous">
|
||||
<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 ng-show="!loadingmyrepos && myrepos.length > 0">
|
||||
<h2>Your Top Repositories</h2>
|
||||
|
@ -44,10 +44,10 @@
|
|||
<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>
|
||||
<span class="landing-social-alternate">
|
||||
<i class="icon-circle"></i>
|
||||
<i class="fa fa-circle"></i>
|
||||
<span class="inner-text">OR</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>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -61,14 +61,15 @@
|
|||
<div ng-show="!user.anonymous" class="user-welcome">
|
||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
||||
<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> <!-- col -->
|
||||
</div> <!-- row -->
|
||||
|
||||
<div class="row" ng-show="user.anonymous">
|
||||
<div class="col-md-4 shoutout">
|
||||
<i class="icon-lock"></i>
|
||||
<i class="fa fa-lock"></i>
|
||||
<b>Secure</b>
|
||||
<span class="shoutout-expand">
|
||||
Store your private docker containers where only you and your team
|
||||
|
@ -77,7 +78,7 @@
|
|||
</div>
|
||||
|
||||
<div class="col-md-4 shoutout">
|
||||
<i class="icon-user"></i>
|
||||
<i class="fa fa-user"></i>
|
||||
<b>Shareable</b>
|
||||
<span class="shoutout-expand">
|
||||
Have to share a repository? No problem! Share with anyone you choose
|
||||
|
@ -85,7 +86,7 @@
|
|||
</div>
|
||||
|
||||
<div class="col-md-4 shoutout">
|
||||
<i class="icon-cloud"></i>
|
||||
<i class="fa fa-cloud"></i>
|
||||
<b>Cloud Hosted</b>
|
||||
<span class="shoutout-expand">
|
||||
Accessible from anywhere, anytime
|
||||
|
@ -97,7 +98,7 @@
|
|||
|
||||
<div class="product-tour container" ng-show="user.anonymous">
|
||||
<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>
|
||||
|
||||
|
|
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>
|
||||
|
||||
<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 class="container" ng-show="!loading && (!repo || !permissions)">
|
||||
|
@ -12,7 +12,7 @@
|
|||
|
||||
<div class="container repo repo-admin" ng-show="!loading && repo && permissions">
|
||||
<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>
|
||||
<span class="repo-circle no-background" repo="repo"></span> <span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
||||
</h3>
|
||||
|
@ -22,7 +22,7 @@
|
|||
<div class="panel panel-default">
|
||||
<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 class="panel-body">
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
|||
|
||||
<tr ng-repeat="(username, permission) in permissions">
|
||||
<td class="user">
|
||||
<i class="icon-user"></i>
|
||||
<i class="fa fa-user"></i>
|
||||
<span>{{username}}</span>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
|
@ -50,7 +50,7 @@
|
|||
<td>
|
||||
<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>
|
||||
<i class="icon-remove"></i>
|
||||
<i class="fa fa-remove"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -68,7 +68,7 @@
|
|||
<div class="panel panel-default">
|
||||
<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 class="panel-body">
|
||||
<form name="createTokenForm" ng-submit="createToken()">
|
||||
|
@ -83,7 +83,7 @@
|
|||
|
||||
<tr ng-repeat="(code, token) in tokens">
|
||||
<td class="user token">
|
||||
<i class="icon-key"></i>
|
||||
<i class="fa fa-key"></i>
|
||||
<a ng-click="showToken(token.code)">{{ token.friendlyName }}</a>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
|
@ -95,7 +95,7 @@
|
|||
<td>
|
||||
<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>
|
||||
<i class="icon-remove"></i>
|
||||
<i class="fa fa-remove"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -118,7 +118,7 @@
|
|||
<div class="panel-heading">Repository Settings</div>
|
||||
<div class="panel-body">
|
||||
<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.
|
||||
|
||||
|
@ -128,7 +128,7 @@
|
|||
</div>
|
||||
|
||||
<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.
|
||||
|
||||
|
@ -175,7 +175,7 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<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 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>
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
<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 class="container ready-indicator" ng-show="!loading" data-status="{{ loading ? '' : 'ready' }}">
|
||||
<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>
|
||||
<div ng-show="private_repositories.length > 0">
|
||||
<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>
|
||||
|
||||
<span class="social-alternate">
|
||||
<i class="icon-circle"></i>
|
||||
<i class="fa fa-circle"></i>
|
||||
<span class="inner-text">OR</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>
|
||||
|
||||
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="container user-admin">
|
||||
<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 class="row" ng-show="errorMessage">
|
||||
<div class="col-md-12">
|
||||
|
@ -18,7 +18,7 @@
|
|||
<div class="panel-heading">
|
||||
{{ plan.title }}
|
||||
<span class="pull-right" ng-show="subscription.plan == plan.stripeId">
|
||||
<i class="icon-ok"></i>
|
||||
<i class="fa fa-ok"></i>
|
||||
Subscribed
|
||||
</span>
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<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 class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<h3>Welcome <b>{{ user.username }}</b>. Your account is fully activated!</h3>
|
||||
|
||||
<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 ng-show="!user.anonymous && !user.verified">
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
</div>
|
||||
|
||||
<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 class="container repo" ng-show="!loading && repo">
|
||||
|
@ -16,20 +16,19 @@
|
|||
|
||||
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings">
|
||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
||||
<i class="icon-cog icon-large"></i>
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
</a>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
|
||||
<!-- Pull command -->
|
||||
<div class="pull-command">
|
||||
Get this repository:
|
||||
|
||||
<div class="pull-command visible-md visible-lg" style="display: none;">
|
||||
<span class="pull-command-title">Pull repository:</span>
|
||||
<div class="pull-container">
|
||||
<div class="input-group">
|
||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||
<i class="icon-copy"></i>
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,17 +39,36 @@
|
|||
</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 -->
|
||||
<p ng-class="'description lead ' + (repo.can_write ? 'editable' : 'noteditable')" ng-click="editDescription()">
|
||||
<span class="content" ng-bind-html-unsafe="getMarkedDown(repo.description)"></span>
|
||||
<i class="icon-edit"></i>
|
||||
</p>
|
||||
<div class="description">
|
||||
<p ng-class="'lead ' + (repo.can_write ? 'editable' : 'noteditable')" ng-click="editDescription()">
|
||||
<span class="content" ng-bind-html-unsafe="getMarkedDown(repo.description)"></span>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<!-- Image History -->
|
||||
<div id="image-history">
|
||||
|
@ -61,22 +79,22 @@
|
|||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<!-- Tag dropdown -->
|
||||
<span class="tag-dropdown dropdown" title="Tags">
|
||||
<i class="icon-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
|
||||
<div class="tag-dropdown dropdown" title="Tags">
|
||||
<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>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="tag in repo.tags">
|
||||
<a href="javascript:void(0)" ng-click="setTag(tag.name)">{{tag.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
<span class="right-title">Tags</span>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Image history loading -->
|
||||
<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>
|
||||
|
||||
<!-- Tree View itself -->
|
||||
|
@ -89,15 +107,15 @@
|
|||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<!-- Image dropdown -->
|
||||
<span class="tag-dropdown dropdown" title="Images">
|
||||
<i class="icon-archive"><span class="tag-count">{{imageHistory.length}}</span></i>
|
||||
<div class="tag-dropdown dropdown" title="Images">
|
||||
<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>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="image in imageHistory">
|
||||
<a href="javascript:void(0)" ng-click="setImage(image)">{{image.id.substr(0, 12)}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
<span class="right-title">Image</span>
|
||||
</div>
|
||||
|
||||
|
@ -117,22 +135,22 @@
|
|||
|
||||
<!-- Image changes loading -->
|
||||
<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 class="changes-container small-changes-container"
|
||||
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">
|
||||
<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>
|
||||
</span>
|
||||
<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>
|
||||
</span>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -140,15 +158,15 @@
|
|||
<div id="collapseChanges" class="panel-collapse collapse in">
|
||||
<div class="well well-sm">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Hosted private docker repositories. Includes full user management and history. Free for public repositories.">
|
||||
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/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-theme.min.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/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-mixpanel.js"></script>
|
||||
|
||||
|
@ -50,6 +51,10 @@
|
|||
|
||||
<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 %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -24,14 +24,10 @@
|
|||
|
||||
<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/browser-chrome.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body_content %}
|
||||
<div ng-view></div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
Binary file not shown.
|
@ -7,10 +7,10 @@ import argparse
|
|||
from apscheduler.scheduler import Scheduler
|
||||
|
||||
from data.queue import image_diff_queue
|
||||
from data.database import db as db_connection
|
||||
from endpoints.registry import process_image_changes
|
||||
|
||||
|
||||
|
||||
root_logger = logging.getLogger('')
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
@ -38,6 +38,10 @@ def process_work_items():
|
|||
|
||||
logger.debug('No more work.')
|
||||
|
||||
if not db_connection.is_closed():
|
||||
logger.debug('Closing thread db connection.')
|
||||
db_connection.close()
|
||||
|
||||
|
||||
def start_worker():
|
||||
logger.debug("Scheduling worker.")
|
||||
|
@ -59,11 +63,6 @@ parser.add_argument('--log', default='diffsworker.log',
|
|||
args = parser.parse_args()
|
||||
|
||||
|
||||
# if not args.D:
|
||||
# else:
|
||||
# logging.basicConfig(format=FORMAT, level=logging.DEBUG)
|
||||
# start_worker(args)
|
||||
|
||||
if args.D:
|
||||
handler = logging.FileHandler(args.log)
|
||||
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