Merge branch 'master' into git
14
Dockerfile
|
@ -38,19 +38,13 @@ ADD . .
|
|||
# Run grunt
|
||||
RUN cd grunt && grunt
|
||||
|
||||
ADD conf/init/svlogd_config /svlogd_config
|
||||
ADD conf/init/doupdatelimits.sh /etc/my_init.d/
|
||||
ADD conf/init/preplogsdir.sh /etc/my_init.d/
|
||||
ADD conf/init/copy_syslog_config.sh /etc/my_init.d/
|
||||
ADD conf/init/runmigration.sh /etc/my_init.d/
|
||||
|
||||
ADD conf/init/gunicorn_web /etc/service/gunicorn_web
|
||||
ADD conf/init/gunicorn_registry /etc/service/gunicorn_registry
|
||||
ADD conf/init/gunicorn_verbs /etc/service/gunicorn_verbs
|
||||
ADD conf/init/nginx /etc/service/nginx
|
||||
ADD conf/init/diffsworker /etc/service/diffsworker
|
||||
ADD conf/init/notificationworker /etc/service/notificationworker
|
||||
ADD conf/init/buildlogsarchiver /etc/service/buildlogsarchiver
|
||||
ADD conf/init/buildmanager /etc/service/buildmanager
|
||||
ADD conf/init/service/ /etc/service/
|
||||
|
||||
RUN rm -rf /etc/service/syslog-forwarder
|
||||
|
||||
# Download any external libs.
|
||||
RUN mkdir static/fonts static/ldn
|
||||
|
|
1
app.py
|
@ -39,7 +39,6 @@ OVERRIDE_CONFIG_YAML_FILENAME = 'conf/stack/config.yaml'
|
|||
OVERRIDE_CONFIG_PY_FILENAME = 'conf/stack/config.py'
|
||||
|
||||
OVERRIDE_CONFIG_KEY = 'QUAY_OVERRIDE_CONFIG'
|
||||
LICENSE_FILENAME = 'conf/stack/license.enc'
|
||||
|
||||
CONFIG_PROVIDER = FileConfigProvider(OVERRIDE_CONFIG_DIRECTORY, 'config.yaml', 'config.py')
|
||||
|
||||
|
|
|
@ -114,7 +114,8 @@ def _process_basic_auth(auth):
|
|||
logger.debug('Invalid robot or password for robot: %s' % credentials[0])
|
||||
|
||||
else:
|
||||
authenticated = authentication.verify_user(credentials[0], credentials[1])
|
||||
(authenticated, error_message) = authentication.verify_user(credentials[0], credentials[1],
|
||||
basic_auth=True)
|
||||
|
||||
if authenticated:
|
||||
logger.debug('Successfully validated user: %s' % authenticated.username)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import hashlib
|
||||
import math
|
||||
|
||||
class Avatar(object):
|
||||
def __init__(self, app=None):
|
||||
|
@ -7,8 +8,7 @@ class Avatar(object):
|
|||
|
||||
def _init_app(self, app):
|
||||
return AVATAR_CLASSES[app.config.get('AVATAR_KIND', 'Gravatar')](
|
||||
app.config['SERVER_HOSTNAME'],
|
||||
app.config['PREFERRED_URL_SCHEME'])
|
||||
app.config['PREFERRED_URL_SCHEME'], app.config['AVATAR_COLORS'], app.config['HTTPCLIENT'])
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.state, name, None)
|
||||
|
@ -16,48 +16,83 @@ class Avatar(object):
|
|||
|
||||
class BaseAvatar(object):
|
||||
""" Base class for all avatar implementations. """
|
||||
def __init__(self, server_hostname, preferred_url_scheme):
|
||||
self.server_hostname = server_hostname
|
||||
def __init__(self, preferred_url_scheme, colors, http_client):
|
||||
self.preferred_url_scheme = preferred_url_scheme
|
||||
self.colors = colors
|
||||
self.http_client = http_client
|
||||
|
||||
def get_url(self, email, size=16, name=None):
|
||||
""" Returns the full URL for viewing the avatar of the given email address, with
|
||||
an optional size.
|
||||
def get_mail_html(self, name, email_or_id, size=16, kind='user'):
|
||||
""" Returns the full HTML and CSS for viewing the avatar of the given name and email address,
|
||||
with an optional size.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
data = self.get_data(name, email_or_id, kind)
|
||||
url = self._get_url(data['hash'], size) if kind != 'team' else None
|
||||
font_size = size - 6
|
||||
|
||||
def compute_hash(self, email, name=None):
|
||||
""" Computes the avatar hash for the given email address. If the name is given and a default
|
||||
avatar is being computed, the name can be used in place of the email address. """
|
||||
raise NotImplementedError
|
||||
if url is not None:
|
||||
# Try to load the gravatar. If we get a non-404 response, then we use it in place of
|
||||
# the CSS avatar.
|
||||
response = self.http_client.get(url)
|
||||
if response.status_code == 200:
|
||||
return """<img src="%s" width="%s" height="%s" alt="%s"
|
||||
style="vertical-align: middle;">""" % (url, size, size, kind)
|
||||
|
||||
radius = '50%' if kind == 'team' else '0%'
|
||||
letter = 'Ω' if kind == 'team' and data['name'] == 'owners' else data['name'].upper()[0]
|
||||
|
||||
return """
|
||||
<span style="width: %spx; height: %spx; background-color: %s; font-size: %spx;
|
||||
line-height: %spx; margin-left: 2px; margin-right: 2px; display: inline-block;
|
||||
vertical-align: middle; text-align: center; color: white; border-radius: %s">
|
||||
%s
|
||||
</span>
|
||||
""" % (size, size, data['color'], font_size, size, radius, letter)
|
||||
|
||||
def get_data_for_user(self, user):
|
||||
return self.get_data(user.username, user.email, 'robot' if user.robot else 'user')
|
||||
|
||||
def get_data_for_team(self, team):
|
||||
return self.get_data(team.name, team.name, 'team')
|
||||
|
||||
def get_data_for_org(self, org):
|
||||
return self.get_data(org.username, org.email, 'org')
|
||||
|
||||
def get_data(self, name, email_or_id, kind='user'):
|
||||
""" Computes and returns the full data block for the avatar:
|
||||
{
|
||||
'name': name,
|
||||
'hash': The gravatar hash, if any.
|
||||
'color': The color for the avatar
|
||||
}
|
||||
"""
|
||||
colors = self.colors
|
||||
hash_value = hashlib.md5(email_or_id.strip().lower()).hexdigest()
|
||||
|
||||
byte_count = int(math.ceil(math.log(len(colors), 16)))
|
||||
byte_data = hash_value[0:byte_count]
|
||||
hash_color = colors[int(byte_data, 16) % len(colors)]
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
'hash': hash_value,
|
||||
'color': hash_color,
|
||||
'kind': kind
|
||||
}
|
||||
|
||||
def _get_url(self, hash_value, size):
|
||||
""" Returns the URL for displaying the overlay avatar. """
|
||||
return None
|
||||
|
||||
|
||||
class GravatarAvatar(BaseAvatar):
|
||||
""" Avatar system that uses gravatar for generating avatars. """
|
||||
def compute_hash(self, email, name=None):
|
||||
email = email or ""
|
||||
return hashlib.md5(email.strip().lower()).hexdigest()
|
||||
|
||||
def get_url(self, email, size=16, name=None):
|
||||
computed = self.compute_hash(email, name=name)
|
||||
return '%s://www.gravatar.com/avatar/%s?d=identicon&size=%s' % (self.preferred_url_scheme,
|
||||
computed, size)
|
||||
def _get_url(self, hash_value, size=16):
|
||||
return '%s://www.gravatar.com/avatar/%s?d=404&size=%s' % (self.preferred_url_scheme,
|
||||
hash_value, size)
|
||||
|
||||
class LocalAvatar(BaseAvatar):
|
||||
""" Avatar system that uses the local system for generating avatars. """
|
||||
def compute_hash(self, email, name=None):
|
||||
email = email or ""
|
||||
if not name and not email:
|
||||
return ''
|
||||
|
||||
prefix = name if name else email
|
||||
return prefix[0] + hashlib.md5(email.strip().lower()).hexdigest()
|
||||
|
||||
def get_url(self, email, size=16, name=None):
|
||||
computed = self.compute_hash(email, name=name)
|
||||
return '%s://%s/avatar/%s?size=%s' % (self.preferred_url_scheme, self.server_hostname,
|
||||
computed, size)
|
||||
|
||||
pass
|
||||
|
||||
AVATAR_CLASSES = {
|
||||
'gravatar': GravatarAvatar,
|
||||
|
|
|
@ -157,8 +157,12 @@ class EphemeralBuilderManager(BaseManager):
|
|||
|
||||
etcd_host = self._manager_config.get('ETCD_HOST', '127.0.0.1')
|
||||
etcd_port = self._manager_config.get('ETCD_PORT', 2379)
|
||||
etcd_auth = self._manager_config.get('ETCD_CERT_AND_KEY', None)
|
||||
etcd_ca_cert = self._manager_config.get('ETCD_CA_CERT', None)
|
||||
|
||||
etcd_auth = self._manager_config.get('ETCD_CERT_AND_KEY', None)
|
||||
if etcd_auth is not None:
|
||||
etcd_auth = tuple(etcd_auth) # Convert YAML list to a tuple
|
||||
|
||||
etcd_protocol = 'http' if etcd_auth is None else 'https'
|
||||
logger.debug('Connecting to etcd on %s:%s', etcd_host, etcd_port)
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ class BuilderExecutor(object):
|
|||
manager_hostname=manager_hostname,
|
||||
coreos_channel=coreos_channel,
|
||||
worker_tag=self.executor_config['WORKER_TAG'],
|
||||
logentries_token=self.executor_config.get('LOGENTRIES_TOKEN', None),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -12,6 +12,9 @@ write_files:
|
|||
REALM={{ realm }}
|
||||
TOKEN={{ token }}
|
||||
SERVER=wss://{{ manager_hostname }}
|
||||
{% if logentries_token -%}
|
||||
LOGENTRIES_TOKEN={{ logentries_token }}
|
||||
{%- endif %}
|
||||
|
||||
coreos:
|
||||
update:
|
||||
|
@ -19,6 +22,17 @@ coreos:
|
|||
group: {{ coreos_channel }}
|
||||
|
||||
units:
|
||||
- name: systemd-journal-gatewayd.socket
|
||||
command: start
|
||||
enable: yes
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Journal Gateway Service Socket
|
||||
[Socket]
|
||||
ListenStream=/var/run/journald.sock
|
||||
Service=systemd-journal-gatewayd.service
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
{{ dockersystemd('quay-builder',
|
||||
'quay.io/coreos/registry-build-worker',
|
||||
quay_username,
|
||||
|
@ -29,3 +43,10 @@ coreos:
|
|||
flattened=True,
|
||||
restart_policy='no'
|
||||
) | indent(4) }}
|
||||
{% if logentries_token -%}
|
||||
{{ dockersystemd('builder-logs',
|
||||
'quay.io/kelseyhightower/journal-2-logentries',
|
||||
extra_args='--env-file /root/overrides.list -v /run/journald.sock:/run/journald.sock',
|
||||
after_units=['quay-builder.service']
|
||||
) | indent(4) }}
|
||||
{%- endif %}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="146" height="18" fill="#555"/><rect rx="4" x="92" width="54" height="18" fill="#dfb317"/><path fill="#dfb317" d="M92 0h4v18h-4z"/><rect rx="4" width="146" height="18" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="47" y="13" fill="#010101" fill-opacity=".3">Docker Image</text><text x="47" y="12">Docker Image</text><text x="118" y="13" fill="#010101" fill-opacity=".3">building</text><text x="118" y="12">building</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="117" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="117" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h63v20H0z"/><path fill="#dfb317" d="M63 0h54v20H63z"/><path fill="url(#b)" d="M0 0h117v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="32.5" y="15" fill="#010101" fill-opacity=".3">container</text><text x="32.5" y="14">container</text><text x="89" y="15" fill="#010101" fill-opacity=".3">building</text><text x="89" y="14">building</text></g></svg>
|
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 748 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="164" height="18" fill="#555"/><rect rx="4" x="92" width="72" height="18" fill="#e05d44"/><path fill="#e05d44" d="M92 0h4v18h-4z"/><rect rx="4" width="164" height="18" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="47" y="13" fill="#010101" fill-opacity=".3">Docker Image</text><text x="47" y="12">Docker Image</text><text x="127" y="13" fill="#010101" fill-opacity=".3">build failed</text><text x="127" y="12">build failed</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="104" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h63v20H0z"/><path fill="#e05d44" d="M63 0h41v20H63z"/><path fill="url(#b)" d="M0 0h104v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="32.5" y="15" fill="#010101" fill-opacity=".3">container</text><text x="32.5" y="14">container</text><text x="82.5" y="15" fill="#010101" fill-opacity=".3">failed</text><text x="82.5" y="14">failed</text></g></svg>
|
Before Width: | Height: | Size: 844 B After Width: | Height: | Size: 748 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="130" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="130" height="18" fill="#555"/><rect rx="4" x="92" width="38" height="18" fill="#9f9f9f"/><path fill="#9f9f9f" d="M92 0h4v18h-4z"/><rect rx="4" width="130" height="18" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="47" y="13" fill="#010101" fill-opacity=".3">Docker Image</text><text x="47" y="12">Docker Image</text><text x="110" y="13" fill="#010101" fill-opacity=".3">none</text><text x="110" y="12">none</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="101" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="101" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h63v20H0z"/><path fill="#9f9f9f" d="M63 0h38v20H63z"/><path fill="url(#b)" d="M0 0h101v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="32.5" y="15" fill="#010101" fill-opacity=".3">container</text><text x="32.5" y="14">container</text><text x="81" y="15" fill="#010101" fill-opacity=".3">none</text><text x="81" y="14">none</text></g></svg>
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 740 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="135" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="135" height="18" fill="#555"/><rect rx="4" x="92" width="43" height="18" fill="#4c1"/><path fill="#4c1" d="M92 0h4v18h-4z"/><rect rx="4" width="135" height="18" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="47" y="13" fill="#010101" fill-opacity=".3">Docker Image</text><text x="47" y="12">Docker Image</text><text x="112.5" y="13" fill="#010101" fill-opacity=".3">ready</text><text x="112.5" y="12">ready</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="106" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h63v20H0z"/><path fill="#97CA00" d="M63 0h43v20H63z"/><path fill="url(#b)" d="M0 0h106v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="32.5" y="15" fill="#010101" fill-opacity=".3">container</text><text x="32.5" y="14">container</text><text x="83.5" y="15" fill="#010101" fill-opacity=".3">ready</text><text x="83.5" y="14">ready</text></g></svg>
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 746 B |
|
@ -4,7 +4,7 @@ types_hash_max_size 2048;
|
|||
include /usr/local/nginx/conf/mime.types.default;
|
||||
|
||||
default_type application/octet-stream;
|
||||
access_log /var/log/nginx/nginx.access.log;
|
||||
access_log /dev/stdout;
|
||||
sendfile on;
|
||||
|
||||
gzip on;
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd /var/log/buildlogsarchiver/
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd /var/log/buildmanager/
|
6
conf/init/copy_syslog_config.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
#! /bin/sh
|
||||
|
||||
if [ -e /conf/stack/syslog-ng-extra.conf ]
|
||||
then
|
||||
cp /conf/stack/syslog-ng-extra.conf /etc/syslog-ng/conf.d/
|
||||
fi
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd /var/log/diffsworker/
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd /var/log/gunicorn_registry/
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd /var/log/gunicorn_verbs/
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd /var/log/gunicorn_web/
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd /var/log/nginx/
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
exec svlogd -t /var/log/notificationworker/
|
|
@ -1,10 +0,0 @@
|
|||
#! /bin/sh
|
||||
|
||||
echo 'Linking config files to logs directory'
|
||||
for svc in `ls /etc/service/`
|
||||
do
|
||||
if [ ! -d /var/log/$svc ]; then
|
||||
mkdir -p /var/log/$svc
|
||||
ln -s /svlogd_config /var/log/$svc/config
|
||||
fi
|
||||
done
|
2
conf/init/service/buildlogsarchiver/log/run
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec logger -i -t buildlogsarchiver
|
2
conf/init/service/buildmanager/log/run
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec logger -i -t buildmanager
|
2
conf/init/service/diffsworker/log/run
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec logger -i -t diffsworker
|
2
conf/init/service/gunicorn_registry/log/run
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec logger -i -t gunicorn_registry
|
2
conf/init/service/gunicorn_verbs/log/run
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec logger -i -t gunicorn_verbs
|
2
conf/init/service/gunicorn_web/log/run
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec logger -i -t gunicorn_web
|
2
conf/init/service/nginx/log/run
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec logger -i -t nginx
|
2
conf/init/service/notificationworker/log/run
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
exec logger -i -t notificationworker
|
|
@ -1,3 +0,0 @@
|
|||
s100000000
|
||||
t86400
|
||||
n4
|
|
@ -5,4 +5,4 @@ real_ip_header proxy_protocol;
|
|||
log_format elb_pp '$proxy_protocol_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent"';
|
||||
access_log /var/log/nginx/nginx.access.log elb_pp;
|
||||
access_log /dev/stdout elb_pp;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# vim: ft=nginx
|
||||
|
||||
pid /tmp/nginx.pid;
|
||||
error_log /var/log/nginx/nginx.error.log;
|
||||
error_log /dev/stdout;
|
||||
|
||||
worker_processes 2;
|
||||
worker_priority -10;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# vim: ft=nginx
|
||||
|
||||
client_body_temp_path /var/log/nginx/client_body 1 2;
|
||||
server_name _;
|
||||
|
||||
keepalive_timeout 5;
|
||||
|
@ -36,7 +35,7 @@ location /v1/repositories/ {
|
|||
|
||||
proxy_pass http://registry_app_server;
|
||||
proxy_read_timeout 2000;
|
||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||
proxy_temp_path /tmp 1 2;
|
||||
|
||||
limit_req zone=repositories burst=10;
|
||||
}
|
||||
|
@ -47,7 +46,7 @@ location /v1/ {
|
|||
proxy_request_buffering off;
|
||||
|
||||
proxy_pass http://registry_app_server;
|
||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||
proxy_temp_path /tmp 1 2;
|
||||
|
||||
client_max_body_size 20G;
|
||||
}
|
||||
|
@ -58,7 +57,7 @@ location /c1/ {
|
|||
proxy_request_buffering off;
|
||||
|
||||
proxy_pass http://verbs_app_server;
|
||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||
proxy_temp_path /tmp 1 2;
|
||||
|
||||
limit_req zone=verbs burst=10;
|
||||
}
|
||||
|
|
14
config.py
|
@ -45,8 +45,6 @@ class DefaultConfig(object):
|
|||
PREFERRED_URL_SCHEME = 'http'
|
||||
SERVER_HOSTNAME = 'localhost:5000'
|
||||
|
||||
AVATAR_KIND = 'local'
|
||||
|
||||
REGISTRY_TITLE = 'CoreOS Enterprise Registry'
|
||||
REGISTRY_TITLE_SHORT = 'Enterprise Registry'
|
||||
|
||||
|
@ -165,6 +163,10 @@ class DefaultConfig(object):
|
|||
# Feature Flag: Whether users can be renamed
|
||||
FEATURE_USER_RENAME = False
|
||||
|
||||
# Feature Flag: Whether non-encrypted passwords (as opposed to encrypted tokens) can be used for
|
||||
# basic auth.
|
||||
FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = False
|
||||
|
||||
BUILD_MANAGER = ('enterprise', {})
|
||||
|
||||
DISTRIBUTED_STORAGE_CONFIG = {
|
||||
|
@ -201,3 +203,11 @@ class DefaultConfig(object):
|
|||
|
||||
# Signed registry grant token expiration in seconds
|
||||
SIGNED_GRANT_EXPIRATION_SEC = 60 * 60 * 24 # One day to complete a push/pull
|
||||
|
||||
# The various avatar background colors.
|
||||
AVATAR_KIND = 'local'
|
||||
AVATAR_COLORS = ['#969696', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728',
|
||||
'#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2',
|
||||
'#7f7f7f', '#c7c7c7', '#bcbd22', '#1f77b4', '#17becf', '#9edae5', '#393b79',
|
||||
'#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b',
|
||||
'#8c6d31', '#ad494a', '#e7ba52', '#a55194']
|
||||
|
|
|
@ -139,7 +139,7 @@ def uuid_generator():
|
|||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
_get_epoch_timestamp = lambda: int(time.time())
|
||||
get_epoch_timestamp = lambda: int(time.time())
|
||||
|
||||
|
||||
def close_db_filter(_):
|
||||
|
@ -167,6 +167,17 @@ class BaseModel(ReadSlaveModel):
|
|||
database = db
|
||||
read_slaves = (read_slave,)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
""" Adds _id accessors so that foreign key field IDs can be looked up without making
|
||||
a database roundtrip.
|
||||
"""
|
||||
if name.endswith('_id'):
|
||||
field_name = name[0:len(name) - 3]
|
||||
if field_name in self._meta.fields:
|
||||
return self._data.get(field_name)
|
||||
|
||||
return super(BaseModel, self).__getattribute__(name)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
uuid = CharField(default=uuid_generator, max_length=36, null=True)
|
||||
|
@ -484,7 +495,7 @@ class RepositoryTag(BaseModel):
|
|||
name = CharField()
|
||||
image = ForeignKeyField(Image)
|
||||
repository = ForeignKeyField(Repository)
|
||||
lifetime_start_ts = IntegerField(default=_get_epoch_timestamp)
|
||||
lifetime_start_ts = IntegerField(default=get_epoch_timestamp)
|
||||
lifetime_end_ts = IntegerField(null=True, index=True)
|
||||
hidden = BooleanField(default=False)
|
||||
|
||||
|
@ -493,6 +504,9 @@ class RepositoryTag(BaseModel):
|
|||
read_slaves = (read_slave,)
|
||||
indexes = (
|
||||
(('repository', 'name'), False),
|
||||
|
||||
# This unique index prevents deadlocks when concurrently moving and deleting tags
|
||||
(('repository', 'name', 'lifetime_end_ts'), True),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ up_mysql() {
|
|||
|
||||
down_mysql() {
|
||||
docker kill mysql
|
||||
docker rm mysql
|
||||
docker rm -v mysql
|
||||
}
|
||||
|
||||
up_mariadb() {
|
||||
|
@ -36,24 +36,24 @@ up_mariadb() {
|
|||
|
||||
down_mariadb() {
|
||||
docker kill mariadb
|
||||
docker rm mariadb
|
||||
docker rm -v mariadb
|
||||
}
|
||||
|
||||
up_percona() {
|
||||
# Run a SQL database on port 3306 inside of Docker.
|
||||
docker run --name percona -p 3306:3306 -d dockerfile/percona
|
||||
docker run --name percona -p 3306:3306 -d percona
|
||||
|
||||
# Sleep for 10s
|
||||
echo 'Sleeping for 10...'
|
||||
sleep 10
|
||||
|
||||
# Add the daabase to mysql.
|
||||
docker run --rm --link percona:percona dockerfile/percona sh -c 'echo "create database genschema" | mysql -h $PERCONA_PORT_3306_TCP_ADDR'
|
||||
docker run --rm --link percona:percona percona sh -c 'echo "create database genschema" | mysql -h $PERCONA_PORT_3306_TCP_ADDR'
|
||||
}
|
||||
|
||||
down_percona() {
|
||||
docker kill percona
|
||||
docker rm percona
|
||||
docker rm -v percona
|
||||
}
|
||||
|
||||
up_postgres() {
|
||||
|
@ -70,7 +70,7 @@ up_postgres() {
|
|||
|
||||
down_postgres() {
|
||||
docker kill postgres
|
||||
docker rm postgres
|
||||
docker rm -v postgres
|
||||
}
|
||||
|
||||
gen_migrate() {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
"""Add a unique index to prevent deadlocks with tags.
|
||||
|
||||
Revision ID: 2b4dc0818a5e
|
||||
Revises: 2b2529fd23ff
|
||||
Create Date: 2015-03-20 23:37:10.558179
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2b4dc0818a5e'
|
||||
down_revision = '2b2529fd23ff'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade(tables):
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index('repositorytag_repository_id_name_lifetime_end_ts', 'repositorytag', ['repository_id', 'name', 'lifetime_end_ts'], unique=True)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade(tables):
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('repositorytag_repository_id_name_lifetime_end_ts', table_name='repositorytag')
|
||||
### end Alembic commands ###
|
|
@ -18,7 +18,7 @@ from data.database import (User, Repository, Image, AccessToken, Role, Repositor
|
|||
DerivedImageStorage, ImageStorageTransformation, random_string_generator,
|
||||
db, BUILD_PHASE, QuayUserField, ImageStorageSignature, QueueItem,
|
||||
ImageStorageSignatureKind, validate_database_url, db_for_update,
|
||||
AccessTokenKind, Star)
|
||||
AccessTokenKind, Star, get_epoch_timestamp)
|
||||
from peewee import JOIN_LEFT_OUTER, fn
|
||||
from util.validation import (validate_username, validate_email, validate_password,
|
||||
INVALID_PASSWORD_MESSAGE)
|
||||
|
@ -310,11 +310,54 @@ def _list_entity_robots(entity_name):
|
|||
.where(User.robot == True, User.username ** (entity_name + '+%')))
|
||||
|
||||
|
||||
def list_entity_robot_tuples(entity_name):
|
||||
return (_list_entity_robots(entity_name)
|
||||
.select(User.username, FederatedLogin.service_ident)
|
||||
.tuples())
|
||||
class _TupleWrapper(object):
|
||||
def __init__(self, data, fields):
|
||||
self._data = data
|
||||
self._fields = fields
|
||||
|
||||
def get(self, field):
|
||||
return self._data[self._fields.index(field.name + ':' + field.model_class.__name__)]
|
||||
|
||||
|
||||
class TupleSelector(object):
|
||||
""" Helper class for selecting tuples from a peewee query and easily accessing
|
||||
them as if they were objects.
|
||||
"""
|
||||
def __init__(self, query, fields):
|
||||
self._query = query.select(*fields).tuples()
|
||||
self._fields = [field.name + ':' + field.model_class.__name__ for field in fields]
|
||||
|
||||
def __iter__(self):
|
||||
return self._build_iterator()
|
||||
|
||||
def _build_iterator(self):
|
||||
for tuple_data in self._query:
|
||||
yield _TupleWrapper(tuple_data, self._fields)
|
||||
|
||||
|
||||
|
||||
def list_entity_robot_permission_teams(entity_name):
|
||||
query = (_list_entity_robots(entity_name)
|
||||
.join(RepositoryPermission, JOIN_LEFT_OUTER,
|
||||
on=(RepositoryPermission.user == FederatedLogin.user))
|
||||
.join(Repository, JOIN_LEFT_OUTER)
|
||||
.switch(User)
|
||||
.join(TeamMember, JOIN_LEFT_OUTER)
|
||||
.join(Team, JOIN_LEFT_OUTER))
|
||||
|
||||
fields = [User.username, FederatedLogin.service_ident, Repository.name, Team.name]
|
||||
return TupleSelector(query, fields)
|
||||
|
||||
|
||||
def list_robot_permissions(robot_name):
|
||||
return (RepositoryPermission.select(RepositoryPermission, User, Repository)
|
||||
.join(Repository)
|
||||
.join(Visibility)
|
||||
.switch(RepositoryPermission)
|
||||
.join(Role)
|
||||
.switch(RepositoryPermission)
|
||||
.join(User)
|
||||
.where(User.username == robot_name, User.robot == True))
|
||||
|
||||
def convert_user_to_organization(user, admin_user):
|
||||
# Change the user to an organization.
|
||||
|
@ -636,6 +679,73 @@ def get_user_or_org_by_customer_id(customer_id):
|
|||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_matching_user_namespaces(namespace_prefix, username, limit=10):
|
||||
query = (Repository
|
||||
.select()
|
||||
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
||||
.switch(Repository)
|
||||
.join(Visibility)
|
||||
.switch(Repository)
|
||||
.join(RepositoryPermission, JOIN_LEFT_OUTER)
|
||||
.where(Namespace.username ** (namespace_prefix + '%'))
|
||||
.group_by(Repository.namespace_user, Repository))
|
||||
|
||||
count = 0
|
||||
namespaces = {}
|
||||
for repo in _filter_to_repos_for_user(query, username):
|
||||
if not repo.namespace_user.username in namespaces:
|
||||
namespaces[repo.namespace_user.username] = repo.namespace_user
|
||||
count = count + 1
|
||||
if count >= limit:
|
||||
break
|
||||
|
||||
return namespaces.values()
|
||||
|
||||
def get_matching_user_teams(team_prefix, user, limit=10):
|
||||
query = (Team.select()
|
||||
.join(User)
|
||||
.switch(Team)
|
||||
.join(TeamMember)
|
||||
.where(TeamMember.user == user, Team.name ** (team_prefix + '%'))
|
||||
.distinct(Team.id)
|
||||
.limit(limit))
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_matching_robots(name_prefix, username, limit=10):
|
||||
admined_orgs = (get_user_organizations(username)
|
||||
.switch(Team)
|
||||
.join(TeamRole)
|
||||
.where(TeamRole.name == 'admin'))
|
||||
|
||||
prefix_checks = False
|
||||
|
||||
for org in admined_orgs:
|
||||
prefix_checks = prefix_checks | (User.username ** (org.username + '+' + name_prefix + '%'))
|
||||
|
||||
prefix_checks = prefix_checks | (User.username ** (username + '+' + name_prefix + '%'))
|
||||
|
||||
return User.select().where(prefix_checks).limit(limit)
|
||||
|
||||
|
||||
def get_matching_admined_teams(team_prefix, user, limit=10):
|
||||
admined_orgs = (get_user_organizations(user.username)
|
||||
.switch(Team)
|
||||
.join(TeamRole)
|
||||
.where(TeamRole.name == 'admin'))
|
||||
|
||||
query = (Team.select()
|
||||
.join(User)
|
||||
.switch(Team)
|
||||
.join(TeamMember)
|
||||
.where(Team.name ** (team_prefix + '%'), Team.organization << (admined_orgs))
|
||||
.distinct(Team.id)
|
||||
.limit(limit))
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_matching_teams(team_prefix, organization):
|
||||
query = Team.select().where(Team.name ** (team_prefix + '%'),
|
||||
Team.organization == organization)
|
||||
|
@ -654,13 +764,13 @@ def get_matching_users(username_prefix, robot_namespace=None,
|
|||
(User.robot == True)))
|
||||
|
||||
query = (User
|
||||
.select(User.username, User.robot)
|
||||
.group_by(User.username, User.robot)
|
||||
.select(User.username, User.email, User.robot)
|
||||
.group_by(User.username, User.email, User.robot)
|
||||
.where(direct_user_query))
|
||||
|
||||
if organization:
|
||||
query = (query
|
||||
.select(User.username, User.robot, fn.Sum(Team.id))
|
||||
.select(User.username, User.email, User.robot, fn.Sum(Team.id))
|
||||
.join(TeamMember, JOIN_LEFT_OUTER)
|
||||
.join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
|
||||
(Team.organization == organization))))
|
||||
|
@ -669,9 +779,11 @@ def get_matching_users(username_prefix, robot_namespace=None,
|
|||
class MatchingUserResult(object):
|
||||
def __init__(self, *args):
|
||||
self.username = args[0]
|
||||
self.is_robot = args[1]
|
||||
self.email = args[1]
|
||||
self.robot = args[2]
|
||||
|
||||
if organization:
|
||||
self.is_org_member = (args[2] != None)
|
||||
self.is_org_member = (args[3] != None)
|
||||
else:
|
||||
self.is_org_member = None
|
||||
|
||||
|
@ -787,7 +899,7 @@ def get_visible_repository_count(username=None, include_public=True,
|
|||
|
||||
|
||||
def get_visible_repositories(username=None, include_public=True, page=None,
|
||||
limit=None, sort=False, namespace=None):
|
||||
limit=None, sort=False, namespace=None, namespace_only=False):
|
||||
query = _visible_repository_query(username=username, include_public=include_public, page=page,
|
||||
limit=limit, namespace=namespace,
|
||||
select_models=[Repository, Namespace, Visibility])
|
||||
|
@ -798,6 +910,9 @@ def get_visible_repositories(username=None, include_public=True, page=None,
|
|||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
if namespace and namespace_only:
|
||||
query = query.where(Namespace.username == namespace)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
@ -876,11 +991,73 @@ def _get_public_repo_visibility():
|
|||
return _public_repo_visibility_cache
|
||||
|
||||
|
||||
def get_matching_repositories(repo_term, username=None):
|
||||
def get_sorted_matching_repositories(prefix, only_public, checker, limit=10):
|
||||
""" Returns repositories matching the given prefix string and passing the given checker
|
||||
function.
|
||||
"""
|
||||
|
||||
last_week = datetime.now() - timedelta(weeks=1)
|
||||
results = []
|
||||
existing_ids = []
|
||||
|
||||
def get_search_results(search_clause, with_count):
|
||||
if len(results) >= limit:
|
||||
return
|
||||
|
||||
selected = [Repository, Namespace]
|
||||
if with_count:
|
||||
selected.append(fn.Count(LogEntry.id).alias('count'))
|
||||
|
||||
query = (Repository.select(*selected)
|
||||
.join(Namespace, JOIN_LEFT_OUTER, on=(Namespace.id == Repository.namespace_user))
|
||||
.switch(Repository)
|
||||
.where(search_clause)
|
||||
.group_by(Repository, Namespace))
|
||||
|
||||
if only_public:
|
||||
query = query.where(Repository.visibility == _get_public_repo_visibility())
|
||||
|
||||
if existing_ids:
|
||||
query = query.where(~(Repository.id << existing_ids))
|
||||
|
||||
if with_count:
|
||||
query = (query.join(LogEntry, JOIN_LEFT_OUTER)
|
||||
.where(LogEntry.datetime >= last_week)
|
||||
.order_by(fn.Count(LogEntry.id).desc()))
|
||||
|
||||
for result in query:
|
||||
if len(results) >= limit:
|
||||
return results
|
||||
|
||||
# Note: We compare IDs here, instead of objects, because calling .visibility on the
|
||||
# Repository will kick off a new SQL query to retrieve that visibility enum value. We don't
|
||||
# join the visibility table in SQL, as well, because it is ungodly slow in MySQL :-/
|
||||
result.is_public = result.visibility_id == _get_public_repo_visibility().id
|
||||
result.count = result.count if with_count else 0
|
||||
|
||||
if not checker(result):
|
||||
continue
|
||||
|
||||
results.append(result)
|
||||
existing_ids.append(result.id)
|
||||
|
||||
# For performance reasons, we conduct the repo name and repo namespace searches on their
|
||||
# own, and with and without counts on their own. This also affords us the ability to give
|
||||
# higher precedence to repository names matching over namespaces, which is semantically correct.
|
||||
get_search_results((Repository.name ** (prefix + '%')), with_count=True)
|
||||
get_search_results((Repository.name ** (prefix + '%')), with_count=False)
|
||||
|
||||
get_search_results((Namespace.username ** (prefix + '%')), with_count=True)
|
||||
get_search_results((Namespace.username ** (prefix + '%')), with_count=False)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def get_matching_repositories(repo_term, username=None, limit=10, include_public=True):
|
||||
namespace_term = repo_term
|
||||
name_term = repo_term
|
||||
|
||||
visible = get_visible_repositories(username)
|
||||
visible = get_visible_repositories(username, include_public=include_public)
|
||||
|
||||
search_clauses = (Repository.name ** ('%' + name_term + '%') |
|
||||
Namespace.username ** ('%' + namespace_term + '%'))
|
||||
|
@ -894,8 +1071,7 @@ def get_matching_repositories(repo_term, username=None):
|
|||
search_clauses = (Repository.name ** ('%' + name_term + '%') &
|
||||
Namespace.username ** ('%' + namespace_term + '%'))
|
||||
|
||||
final = visible.where(search_clauses).limit(10)
|
||||
return list(final)
|
||||
return visible.where(search_clauses).limit(limit)
|
||||
|
||||
|
||||
def change_password(user, new_password):
|
||||
|
@ -905,6 +1081,7 @@ def change_password(user, new_password):
|
|||
pw_hash = hash_password(new_password)
|
||||
user.invalid_login_attempts = 0
|
||||
user.password_hash = pw_hash
|
||||
user.uuid = str(uuid4())
|
||||
user.save()
|
||||
|
||||
# Remove any password required notifications for the user.
|
||||
|
@ -1038,7 +1215,8 @@ def get_all_repo_teams(namespace_name, repository_name):
|
|||
|
||||
|
||||
def get_all_repo_users(namespace_name, repository_name):
|
||||
return (RepositoryPermission.select(User.username, User.robot, Role.name, RepositoryPermission)
|
||||
return (RepositoryPermission.select(User.username, User.email, User.robot, Role.name,
|
||||
RepositoryPermission)
|
||||
.join(User)
|
||||
.switch(RepositoryPermission)
|
||||
.join(Role)
|
||||
|
@ -1577,9 +1755,21 @@ def get_repository_images(namespace_name, repository_name):
|
|||
return _get_repository_images_base(namespace_name, repository_name, lambda q: q)
|
||||
|
||||
|
||||
def _tag_alive(query):
|
||||
def _tag_alive(query, now_ts=None):
|
||||
if now_ts is None:
|
||||
now_ts = get_epoch_timestamp()
|
||||
return query.where((RepositoryTag.lifetime_end_ts >> None) |
|
||||
(RepositoryTag.lifetime_end_ts > int(time.time())))
|
||||
(RepositoryTag.lifetime_end_ts > now_ts))
|
||||
|
||||
|
||||
def list_repository_tag_history(repository, limit=100):
|
||||
query = (RepositoryTag
|
||||
.select(RepositoryTag, Image)
|
||||
.join(Image)
|
||||
.where(RepositoryTag.repository == repository)
|
||||
.order_by(RepositoryTag.lifetime_start_ts.desc())
|
||||
.limit(limit))
|
||||
return query
|
||||
|
||||
|
||||
def list_repository_tags(namespace_name, repository_name, include_hidden=False,
|
||||
|
@ -1610,14 +1800,19 @@ def list_repository_tags(namespace_name, repository_name, include_hidden=False,
|
|||
def _garbage_collect_tags(namespace_name, repository_name):
|
||||
# We do this without using a join to prevent holding read locks on the repository table
|
||||
repo = _get_repository(namespace_name, repository_name)
|
||||
now = int(time.time())
|
||||
expired_time = get_epoch_timestamp() - repo.namespace_user.removed_tag_expiration_s
|
||||
|
||||
(RepositoryTag
|
||||
.delete()
|
||||
.where(RepositoryTag.repository == repo,
|
||||
~(RepositoryTag.lifetime_end_ts >> None),
|
||||
(RepositoryTag.lifetime_end_ts + repo.namespace_user.removed_tag_expiration_s) <= now)
|
||||
.execute())
|
||||
tags_to_delete = list(RepositoryTag
|
||||
.select(RepositoryTag.id)
|
||||
.where(RepositoryTag.repository == repo,
|
||||
~(RepositoryTag.lifetime_end_ts >> None),
|
||||
(RepositoryTag.lifetime_end_ts <= expired_time))
|
||||
.order_by(RepositoryTag.id))
|
||||
if len(tags_to_delete) > 0:
|
||||
(RepositoryTag
|
||||
.delete()
|
||||
.where(RepositoryTag.id << tags_to_delete)
|
||||
.execute())
|
||||
|
||||
|
||||
def garbage_collect_repository(namespace_name, repository_name):
|
||||
|
@ -1713,46 +1908,39 @@ def _garbage_collect_storage(storage_id_whitelist):
|
|||
logger.debug('Garbage collecting storages from candidates: %s', storage_id_whitelist)
|
||||
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||
# Track all of the data that should be removed from blob storage
|
||||
placements_to_remove = orphaned_storage_query(ImageStoragePlacement
|
||||
.select(ImageStoragePlacement,
|
||||
ImageStorage,
|
||||
ImageStorageLocation)
|
||||
.join(ImageStorageLocation)
|
||||
.switch(ImageStoragePlacement)
|
||||
.join(ImageStorage),
|
||||
storage_id_whitelist,
|
||||
(ImageStorage, ImageStoragePlacement,
|
||||
ImageStorageLocation))
|
||||
placements_to_remove = list(orphaned_storage_query(ImageStoragePlacement
|
||||
.select(ImageStoragePlacement,
|
||||
ImageStorage,
|
||||
ImageStorageLocation)
|
||||
.join(ImageStorageLocation)
|
||||
.switch(ImageStoragePlacement)
|
||||
.join(ImageStorage),
|
||||
storage_id_whitelist,
|
||||
(ImageStorage, ImageStoragePlacement,
|
||||
ImageStorageLocation)))
|
||||
|
||||
paths_to_remove = placements_query_to_paths_set(placements_to_remove.clone())
|
||||
paths_to_remove = placements_query_to_paths_set(placements_to_remove)
|
||||
|
||||
# Remove the placements for orphaned storages
|
||||
placements_subquery = (placements_to_remove
|
||||
.clone()
|
||||
.select(ImageStoragePlacement.id)
|
||||
.alias('ps'))
|
||||
inner = (ImageStoragePlacement
|
||||
.select(placements_subquery.c.id)
|
||||
.from_(placements_subquery))
|
||||
placements_removed = (ImageStoragePlacement
|
||||
.delete()
|
||||
.where(ImageStoragePlacement.id << inner)
|
||||
.execute())
|
||||
logger.debug('Removed %s image storage placements', placements_removed)
|
||||
if len(placements_to_remove) > 0:
|
||||
placement_ids_to_remove = [placement.id for placement in placements_to_remove]
|
||||
placements_removed = (ImageStoragePlacement
|
||||
.delete()
|
||||
.where(ImageStoragePlacement.id << placement_ids_to_remove)
|
||||
.execute())
|
||||
logger.debug('Removed %s image storage placements', placements_removed)
|
||||
|
||||
# Remove all orphaned storages
|
||||
# The comma after ImageStorage.id is VERY important, it makes it a tuple, which is a sequence
|
||||
orphaned_storages = orphaned_storage_query(ImageStorage.select(ImageStorage.id),
|
||||
storage_id_whitelist,
|
||||
(ImageStorage.id,)).alias('osq')
|
||||
orphaned_storage_inner = (ImageStorage
|
||||
.select(orphaned_storages.c.id)
|
||||
.from_(orphaned_storages))
|
||||
storages_removed = (ImageStorage
|
||||
.delete()
|
||||
.where(ImageStorage.id << orphaned_storage_inner)
|
||||
.execute())
|
||||
logger.debug('Removed %s image storage records', storages_removed)
|
||||
orphaned_storages = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id),
|
||||
storage_id_whitelist,
|
||||
(ImageStorage.id,)).alias('osq'))
|
||||
if len(orphaned_storages) > 0:
|
||||
storages_removed = (ImageStorage
|
||||
.delete()
|
||||
.where(ImageStorage.id << orphaned_storages)
|
||||
.execute())
|
||||
logger.debug('Removed %s image storage records', storages_removed)
|
||||
|
||||
# We are going to make the conscious decision to not delete image storage blobs inside
|
||||
# transactions.
|
||||
|
@ -1803,40 +1991,34 @@ def get_parent_images(namespace_name, repository_name, image_obj):
|
|||
|
||||
def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||
tag_docker_image_id):
|
||||
try:
|
||||
repo = _get_repository(namespace_name, repository_name)
|
||||
except Repository.DoesNotExist:
|
||||
raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name))
|
||||
|
||||
now_ts = get_epoch_timestamp()
|
||||
|
||||
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||
try:
|
||||
repo = _get_repository(namespace_name, repository_name)
|
||||
except Repository.DoesNotExist:
|
||||
raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name))
|
||||
tag = db_for_update(_tag_alive(RepositoryTag
|
||||
.select()
|
||||
.where(RepositoryTag.repository == repo,
|
||||
RepositoryTag.name == tag_name), now_ts)).get()
|
||||
tag.lifetime_end_ts = now_ts
|
||||
tag.save()
|
||||
except RepositoryTag.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
image = Image.get(Image.docker_image_id == tag_docker_image_id, Image.repository == repo)
|
||||
except Image.DoesNotExist:
|
||||
raise DataModelException('Invalid image with id: %s' % tag_docker_image_id)
|
||||
|
||||
now_ts = int(time.time())
|
||||
|
||||
created = RepositoryTag.create(repository=repo, image=image, name=tag_name,
|
||||
lifetime_start_ts=now_ts)
|
||||
|
||||
try:
|
||||
# When we move a tag, we really end the timeline of the old one and create a new one
|
||||
query = _tag_alive(RepositoryTag
|
||||
.select()
|
||||
.where(RepositoryTag.repository == repo, RepositoryTag.name == tag_name,
|
||||
RepositoryTag.id != created.id))
|
||||
tag = query.get()
|
||||
tag.lifetime_end_ts = now_ts
|
||||
tag.save()
|
||||
except RepositoryTag.DoesNotExist:
|
||||
# No tag that needs to be ended
|
||||
pass
|
||||
|
||||
return created
|
||||
|
||||
return RepositoryTag.create(repository=repo, image=image, name=tag_name,
|
||||
lifetime_start_ts=now_ts)
|
||||
|
||||
def delete_tag(namespace_name, repository_name, tag_name):
|
||||
now_ts = get_epoch_timestamp()
|
||||
with config.app_config['DB_TRANSACTION_FACTORY'](db):
|
||||
try:
|
||||
query = _tag_alive(RepositoryTag
|
||||
|
@ -1845,21 +2027,21 @@ def delete_tag(namespace_name, repository_name, tag_name):
|
|||
.join(Namespace, on=(Repository.namespace_user == Namespace.id))
|
||||
.where(Repository.name == repository_name,
|
||||
Namespace.username == namespace_name,
|
||||
RepositoryTag.name == tag_name))
|
||||
RepositoryTag.name == tag_name), now_ts)
|
||||
found = db_for_update(query).get()
|
||||
except RepositoryTag.DoesNotExist:
|
||||
msg = ('Invalid repository tag \'%s\' on repository \'%s/%s\'' %
|
||||
(tag_name, namespace_name, repository_name))
|
||||
raise DataModelException(msg)
|
||||
|
||||
found.lifetime_end_ts = int(time.time())
|
||||
found.lifetime_end_ts = now_ts
|
||||
found.save()
|
||||
|
||||
|
||||
def create_temporary_hidden_tag(repo, image, expiration_s):
|
||||
""" Create a tag with a defined timeline, that will not appear in the UI or CLI. Returns the name
|
||||
of the temporary tag. """
|
||||
now_ts = int(time.time())
|
||||
now_ts = get_epoch_timestamp()
|
||||
expire_ts = now_ts + expiration_s
|
||||
tag_name = str(uuid4())
|
||||
RepositoryTag.create(repository=repo, image=image, name=tag_name, lifetime_start_ts=now_ts,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import redis
|
||||
import json
|
||||
import threading
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class UserEventBuilder(object):
|
||||
"""
|
||||
|
@ -68,8 +71,9 @@ class UserEvent(object):
|
|||
def conduct():
|
||||
try:
|
||||
self.publish_event_data_sync(event_id, data_obj)
|
||||
except Exception as e:
|
||||
print e
|
||||
logger.debug('Published user event %s: %s', event_id, data_obj)
|
||||
except Exception:
|
||||
logger.exception('Could not publish user event')
|
||||
|
||||
thread = threading.Thread(target=conduct)
|
||||
thread.start()
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import ldap
|
||||
import logging
|
||||
import json
|
||||
import itertools
|
||||
import uuid
|
||||
import struct
|
||||
|
||||
from util.aes import AESCipher
|
||||
from util.validation import generate_valid_usernames
|
||||
from data import model
|
||||
|
||||
|
@ -106,6 +111,7 @@ class LDAPUsers(object):
|
|||
return found_user is not None
|
||||
|
||||
|
||||
|
||||
class UserAuthentication(object):
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
|
@ -138,5 +144,81 @@ class UserAuthentication(object):
|
|||
app.extensions['authentication'] = users
|
||||
return users
|
||||
|
||||
def _get_secret_key(self):
|
||||
""" Returns the secret key to use for encrypting and decrypting. """
|
||||
from app import app
|
||||
app_secret_key = app.config['SECRET_KEY']
|
||||
secret_key = None
|
||||
|
||||
# First try parsing the key as an int.
|
||||
try:
|
||||
big_int = int(app_secret_key)
|
||||
secret_key = str(bytearray.fromhex('{:02x}'.format(big_int)))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Next try parsing it as an UUID.
|
||||
if secret_key is None:
|
||||
try:
|
||||
secret_key = uuid.UUID(app_secret_key).bytes
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if secret_key is None:
|
||||
secret_key = str(bytearray(map(ord, app_secret_key)))
|
||||
|
||||
# Otherwise, use the bytes directly.
|
||||
return ''.join(itertools.islice(itertools.cycle(secret_key), 32))
|
||||
|
||||
def encrypt_user_password(self, password):
|
||||
""" Returns an encrypted version of the user's password. """
|
||||
data = {
|
||||
'password': password
|
||||
}
|
||||
|
||||
message = json.dumps(data)
|
||||
cipher = AESCipher(self._get_secret_key())
|
||||
return cipher.encrypt(message)
|
||||
|
||||
def _decrypt_user_password(self, encrypted):
|
||||
""" Attempts to decrypt the given password and returns it. """
|
||||
cipher = AESCipher(self._get_secret_key())
|
||||
|
||||
try:
|
||||
message = cipher.decrypt(encrypted)
|
||||
except ValueError:
|
||||
return None
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(message)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return data.get('password', encrypted)
|
||||
|
||||
def verify_user(self, username_or_email, password, basic_auth=False):
|
||||
# First try to decode the password as a signed token.
|
||||
if basic_auth:
|
||||
import features
|
||||
|
||||
decrypted = self._decrypt_user_password(password)
|
||||
if decrypted is None:
|
||||
# This is a normal password.
|
||||
if features.REQUIRE_ENCRYPTED_BASIC_AUTH:
|
||||
msg = ('Client login with unecrypted passwords is disabled. Please generate an ' +
|
||||
'encrypted password in the user admin panel for use here.')
|
||||
return (None, msg)
|
||||
else:
|
||||
password = decrypted
|
||||
|
||||
result = self.state.verify_user(username_or_email, password)
|
||||
if result:
|
||||
return (result, '')
|
||||
else:
|
||||
return (result, 'Invalid password.')
|
||||
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.state, name, None)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<h3>Invitation to join team: {{ teamname }}</h3>
|
||||
|
||||
{{ inviter | user_reference }} has invited you to join the team <b>{{ teamname }}</b> under organization {{ organization | user_reference }}.
|
||||
{{ inviter | user_reference }} has invited you to join the team {{ teamname | team_reference }} under organization {{ organization | user_reference }}.
|
||||
|
||||
<br><br>
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from data import model
|
|||
from util.cache import cache_control_flask_restful
|
||||
|
||||
|
||||
def image_view(image, image_map):
|
||||
def image_view(image, image_map, include_locations=True, include_ancestors=True):
|
||||
extended_props = image
|
||||
if image.storage and image.storage.id:
|
||||
extended_props = image.storage
|
||||
|
@ -20,24 +20,35 @@ def image_view(image, image_map):
|
|||
if not aid or not aid in image_map:
|
||||
return ''
|
||||
|
||||
return image_map[aid]
|
||||
return image_map[aid].docker_image_id
|
||||
|
||||
# Calculate the ancestors string, with the DBID's replaced with the docker IDs.
|
||||
ancestors = [docker_id(a) for a in image.ancestors.split('/')]
|
||||
ancestors_string = '/'.join(ancestors)
|
||||
|
||||
return {
|
||||
image_data = {
|
||||
'id': image.docker_image_id,
|
||||
'created': format_date(extended_props.created),
|
||||
'comment': extended_props.comment,
|
||||
'command': json.loads(command) if command else None,
|
||||
'size': extended_props.image_size,
|
||||
'locations': list(image.storage.locations),
|
||||
'uploading': image.storage.uploading,
|
||||
'ancestors': ancestors_string,
|
||||
'sort_index': len(image.ancestors)
|
||||
'sort_index': len(image.ancestors),
|
||||
}
|
||||
|
||||
if include_locations:
|
||||
image_data['locations'] = list(image.storage.locations)
|
||||
|
||||
if include_ancestors:
|
||||
# Calculate the ancestors string, with the DBID's replaced with the docker IDs.
|
||||
ancestors = [docker_id(a) for a in image.ancestors.split('/')]
|
||||
image_data['ancestors'] = '/'.join(ancestors)
|
||||
|
||||
return image_data
|
||||
|
||||
|
||||
def historical_image_view(image, image_map):
|
||||
ancestors = [image_map[a] for a in image.ancestors.split('/')[1:-1]]
|
||||
normal_view = image_view(image, image_map)
|
||||
normal_view['history'] = [image_view(parent, image_map, False, False) for parent in ancestors]
|
||||
return normal_view
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/image/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
|
@ -62,7 +73,7 @@ class RepositoryImageList(RepositoryParamResource):
|
|||
filtered_images = []
|
||||
for image in all_images:
|
||||
if str(image.id) in found_image_ids:
|
||||
image_map[str(image.id)] = image.docker_image_id
|
||||
image_map[str(image.id)] = image
|
||||
filtered_images.append(image)
|
||||
|
||||
def add_tags(image_json):
|
||||
|
@ -90,9 +101,9 @@ class RepositoryImage(RepositoryParamResource):
|
|||
# Lookup all the ancestor images for the image.
|
||||
image_map = {}
|
||||
for current_image in model.get_parent_images(namespace, repository, image):
|
||||
image_map[str(current_image.id)] = image.docker_image_id
|
||||
image_map[str(current_image.id)] = current_image
|
||||
|
||||
return image_view(image, image_map)
|
||||
return historical_image_view(image, image_map)
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
|
||||
|
|
|
@ -24,16 +24,22 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def org_view(o, teams):
|
||||
admin_org = AdministerOrganizationPermission(o.username)
|
||||
is_admin = admin_org.can()
|
||||
is_admin = AdministerOrganizationPermission(o.username).can()
|
||||
is_member = OrganizationMemberPermission(o.username).can()
|
||||
|
||||
view = {
|
||||
'name': o.username,
|
||||
'email': o.email if is_admin else '',
|
||||
'avatar': avatar.compute_hash(o.email, name=o.username),
|
||||
'teams': {t.name : team_view(o.username, t) for t in teams},
|
||||
'is_admin': is_admin
|
||||
'avatar': avatar.get_data_for_user(o),
|
||||
'is_admin': is_admin,
|
||||
'is_member': is_member
|
||||
}
|
||||
|
||||
if teams is not None:
|
||||
teams = sorted(teams, key=lambda team:team.id)
|
||||
view['teams'] = {t.name : team_view(o.username, t) for t in teams}
|
||||
view['ordered_teams'] = [team.name for team in teams]
|
||||
|
||||
if is_admin:
|
||||
view['invoice_email'] = o.invoice_email
|
||||
|
||||
|
@ -129,17 +135,17 @@ class Organization(ApiResource):
|
|||
@nickname('getOrganization')
|
||||
def get(self, orgname):
|
||||
""" Get the details for the specified organization """
|
||||
permission = OrganizationMemberPermission(orgname)
|
||||
if permission.can():
|
||||
try:
|
||||
org = model.get_organization(orgname)
|
||||
except model.InvalidOrganizationException:
|
||||
raise NotFound()
|
||||
try:
|
||||
org = model.get_organization(orgname)
|
||||
except model.InvalidOrganizationException:
|
||||
raise NotFound()
|
||||
|
||||
teams = None
|
||||
if OrganizationMemberPermission(orgname).can():
|
||||
teams = model.get_teams_within_org(org)
|
||||
return org_view(org, teams)
|
||||
|
||||
raise Unauthorized()
|
||||
return org_view(org, teams)
|
||||
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('changeOrganizationDetails')
|
||||
|
@ -218,7 +224,7 @@ class OrgPrivateRepositories(ApiResource):
|
|||
@path_param('orgname', 'The name of the organization')
|
||||
class OrgnaizationMemberList(ApiResource):
|
||||
""" Resource for listing the members of an organization. """
|
||||
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('getOrganizationMembers')
|
||||
def get(self, orgname):
|
||||
|
@ -297,16 +303,14 @@ class ApplicationInformation(ApiResource):
|
|||
if not application:
|
||||
raise NotFound()
|
||||
|
||||
org_hash = avatar.compute_hash(application.organization.email,
|
||||
name=application.organization.username)
|
||||
app_hash = (avatar.compute_hash(application.avatar_email, name=application.name) if
|
||||
application.avatar_email else org_hash)
|
||||
app_email = application.avatar_email or application.organization.email
|
||||
app_data = avatar.get_data(application.name, app_email, 'app')
|
||||
|
||||
return {
|
||||
'name': application.name,
|
||||
'description': application.description,
|
||||
'uri': application.application_uri,
|
||||
'avatar': app_hash,
|
||||
'avatar': app_data,
|
||||
'organization': org_view(application.organization, [])
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
|||
|
||||
from flask import request
|
||||
|
||||
from app import avatar
|
||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||
log_action, request_error, validate_json_request, path_param)
|
||||
from data import model
|
||||
|
@ -16,7 +17,10 @@ def role_view(repo_perm_obj):
|
|||
}
|
||||
|
||||
def wrap_role_view_user(role_json, user):
|
||||
role_json['name'] = user.username
|
||||
role_json['is_robot'] = user.robot
|
||||
if not user.robot:
|
||||
role_json['avatar'] = avatar.get_data_for_user(user)
|
||||
return role_json
|
||||
|
||||
|
||||
|
@ -25,6 +29,12 @@ def wrap_role_view_org(role_json, user, org_members):
|
|||
return role_json
|
||||
|
||||
|
||||
def wrap_role_view_team(role_json, team):
|
||||
role_json['name'] = team.name
|
||||
role_json['avatar'] = avatar.get_data_for_team(team)
|
||||
return role_json
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/team/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryTeamPermissionList(RepositoryParamResource):
|
||||
|
@ -35,8 +45,11 @@ class RepositoryTeamPermissionList(RepositoryParamResource):
|
|||
""" List all team permission. """
|
||||
repo_perms = model.get_all_repo_teams(namespace, repository)
|
||||
|
||||
def wrapped_role_view(repo_perm):
|
||||
return wrap_role_view_team(role_view(repo_perm), repo_perm.team)
|
||||
|
||||
return {
|
||||
'permissions': {repo_perm.team.name: role_view(repo_perm)
|
||||
'permissions': {repo_perm.team.name: wrapped_role_view(repo_perm)
|
||||
for repo_perm in repo_perms}
|
||||
}
|
||||
|
||||
|
@ -232,7 +245,7 @@ class RepositoryTeamPermission(RepositoryParamResource):
|
|||
'role': new_permission['role']},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
return role_view(perm), 200
|
||||
return wrap_role_view_team(role_view(perm), perm.team), 200
|
||||
|
||||
@require_repo_admin
|
||||
@nickname('deleteTeamPermissions')
|
||||
|
|
|
@ -7,6 +7,7 @@ from auth.permissions import AdministerOrganizationPermission
|
|||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from data import model
|
||||
from app import avatar
|
||||
|
||||
|
||||
def prototype_view(proto, org_members):
|
||||
|
@ -16,6 +17,7 @@ def prototype_view(proto, org_members):
|
|||
'is_robot': user.robot,
|
||||
'kind': 'user',
|
||||
'is_org_member': user.robot or user.username in org_members,
|
||||
'avatar': avatar.get_data_for_user(user)
|
||||
}
|
||||
|
||||
if proto.delegate_user:
|
||||
|
@ -24,6 +26,7 @@ def prototype_view(proto, org_members):
|
|||
delegate_view = {
|
||||
'name': proto.delegate_team.name,
|
||||
'kind': 'team',
|
||||
'avatar': avatar.get_data_for_team(proto.delegate_team)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -109,6 +109,8 @@ class RepositoryList(ApiResource):
|
|||
@query_param('sort', 'Whether to sort the results.', type=truthy_bool, default=False)
|
||||
@query_param('count', 'Whether to include a count of the total number of results available.',
|
||||
type=truthy_bool, default=False)
|
||||
@query_param('namespace_only', 'Whether to limit only to the given namespace.',
|
||||
type=truthy_bool, default=False)
|
||||
def get(self, args):
|
||||
"""Fetch the list of repositories under a variety of situations."""
|
||||
username = None
|
||||
|
@ -129,7 +131,8 @@ class RepositoryList(ApiResource):
|
|||
|
||||
repo_query = model.get_visible_repositories(username, limit=args['limit'], page=args['page'],
|
||||
include_public=args['public'], sort=args['sort'],
|
||||
namespace=args['namespace'])
|
||||
namespace=args['namespace'],
|
||||
namespace_only=args['namespace_only'])
|
||||
def repo_view(repo_obj):
|
||||
repo = {
|
||||
'namespace': repo_obj.namespace_user.username,
|
||||
|
|
|
@ -5,16 +5,63 @@ from auth.permissions import AdministerOrganizationPermission, OrganizationMembe
|
|||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from data import model
|
||||
from data.database import User, Team, Repository, FederatedLogin
|
||||
from util.names import format_robot_username
|
||||
|
||||
from flask import abort
|
||||
from app import avatar
|
||||
|
||||
def robot_view(name, token):
|
||||
return {
|
||||
'name': name,
|
||||
'token': token,
|
||||
'token': token
|
||||
}
|
||||
|
||||
|
||||
def permission_view(permission):
|
||||
return {
|
||||
'repository': {
|
||||
'name': permission.repository.name,
|
||||
'is_public': permission.repository.visibility.name == 'public'
|
||||
},
|
||||
'role': permission.role.name
|
||||
}
|
||||
|
||||
|
||||
def robots_list(prefix):
|
||||
tuples = model.list_entity_robot_permission_teams(prefix)
|
||||
|
||||
robots = {}
|
||||
robot_teams = set()
|
||||
|
||||
for robot_tuple in tuples:
|
||||
robot_name = robot_tuple.get(User.username)
|
||||
if not robot_name in robots:
|
||||
robots[robot_name] = {
|
||||
'name': robot_name,
|
||||
'token': robot_tuple.get(FederatedLogin.service_ident),
|
||||
'teams': [],
|
||||
'repositories': []
|
||||
}
|
||||
|
||||
team_name = robot_tuple.get(Team.name)
|
||||
repository_name = robot_tuple.get(Repository.name)
|
||||
|
||||
if team_name is not None:
|
||||
check_key = robot_name + ':' + team_name
|
||||
if not check_key in robot_teams:
|
||||
robot_teams.add(check_key)
|
||||
|
||||
robots[robot_name]['teams'].append({
|
||||
'name': team_name,
|
||||
'avatar': avatar.get_data(team_name, team_name, 'team')
|
||||
})
|
||||
|
||||
if repository_name is not None:
|
||||
if not repository_name in robots[robot_name]['repositories']:
|
||||
robots[robot_name]['repositories'].append(repository_name)
|
||||
|
||||
return {'robots': robots.values()}
|
||||
|
||||
@resource('/v1/user/robots')
|
||||
@internal_only
|
||||
class UserRobotList(ApiResource):
|
||||
|
@ -24,10 +71,7 @@ class UserRobotList(ApiResource):
|
|||
def get(self):
|
||||
""" List the available robots for the user. """
|
||||
user = get_authenticated_user()
|
||||
robots = model.list_entity_robot_tuples(user.username)
|
||||
return {
|
||||
'robots': [robot_view(name, password) for name, password in robots]
|
||||
}
|
||||
return robots_list(user.username)
|
||||
|
||||
|
||||
@resource('/v1/user/robots/<robot_shortname>')
|
||||
|
@ -73,10 +117,7 @@ class OrgRobotList(ApiResource):
|
|||
""" List the organization's robots. """
|
||||
permission = OrganizationMemberPermission(orgname)
|
||||
if permission.can():
|
||||
robots = model.list_entity_robot_tuples(orgname)
|
||||
return {
|
||||
'robots': [robot_view(name, password) for name, password in robots]
|
||||
}
|
||||
return robots_list(orgname)
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
|
@ -125,6 +166,47 @@ class OrgRobot(ApiResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/user/robots/<robot_shortname>/permissions')
|
||||
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||
@internal_only
|
||||
class UserRobotPermissions(ApiResource):
|
||||
""" Resource for listing the permissions a user's robot has in the system. """
|
||||
@require_user_admin
|
||||
@nickname('getUserRobotPermissions')
|
||||
def get(self, robot_shortname):
|
||||
""" Returns the list of repository permissions for the user's robot. """
|
||||
parent = get_authenticated_user()
|
||||
robot, password = model.get_robot(robot_shortname, parent)
|
||||
permissions = model.list_robot_permissions(robot.username)
|
||||
|
||||
return {
|
||||
'permissions': [permission_view(permission) for permission in permissions]
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/robots/<robot_shortname>/permissions')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||
@related_user_resource(UserRobotPermissions)
|
||||
class OrgRobotPermissions(ApiResource):
|
||||
""" Resource for listing the permissions an org's robot has in the system. """
|
||||
@require_user_admin
|
||||
@nickname('getOrgRobotPermissions')
|
||||
def get(self, orgname, robot_shortname):
|
||||
""" Returns the list of repository permissions for the org's robot. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
parent = model.get_organization(orgname)
|
||||
robot, password = model.get_robot(robot_shortname, parent)
|
||||
permissions = model.list_robot_permissions(robot.username)
|
||||
|
||||
return {
|
||||
'permissions': [permission_view(permission) for permission in permissions]
|
||||
}
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/user/robots/<robot_shortname>/regenerate')
|
||||
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||
@internal_only
|
||||
|
|
|
@ -3,11 +3,15 @@ from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, ni
|
|||
from data import model
|
||||
from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
|
||||
ReadRepositoryPermission, UserAdminPermission,
|
||||
AdministerOrganizationPermission)
|
||||
AdministerOrganizationPermission, ReadRepositoryPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from app import avatar
|
||||
from app import avatar, get_app_url
|
||||
from operator import itemgetter
|
||||
from stringscore import liquidmetal
|
||||
from util.names import parse_robot_username
|
||||
|
||||
import math
|
||||
|
||||
@resource('/v1/entities/<prefix>')
|
||||
class EntitySearch(ApiResource):
|
||||
|
@ -45,7 +49,7 @@ class EntitySearch(ApiResource):
|
|||
'name': namespace_name,
|
||||
'kind': 'org',
|
||||
'is_org_member': True,
|
||||
'avatar': avatar.compute_hash(organization.email, name=organization.username),
|
||||
'avatar': avatar.get_data_for_org(organization),
|
||||
}]
|
||||
|
||||
except model.InvalidOrganizationException:
|
||||
|
@ -63,7 +67,8 @@ class EntitySearch(ApiResource):
|
|||
result = {
|
||||
'name': team.name,
|
||||
'kind': 'team',
|
||||
'is_org_member': True
|
||||
'is_org_member': True,
|
||||
'avatar': avatar.get_data_for_team(team)
|
||||
}
|
||||
return result
|
||||
|
||||
|
@ -71,11 +76,12 @@ class EntitySearch(ApiResource):
|
|||
user_json = {
|
||||
'name': user.username,
|
||||
'kind': 'user',
|
||||
'is_robot': user.is_robot,
|
||||
'is_robot': user.robot,
|
||||
'avatar': avatar.get_data_for_user(user)
|
||||
}
|
||||
|
||||
if organization is not None:
|
||||
user_json['is_org_member'] = user.is_robot or user.is_org_member
|
||||
user_json['is_org_member'] = user.robot or user.is_org_member
|
||||
|
||||
return user_json
|
||||
|
||||
|
@ -128,3 +134,154 @@ class FindRepositories(ApiResource):
|
|||
if (repo.visibility.name == 'public' or
|
||||
ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())]
|
||||
}
|
||||
|
||||
|
||||
|
||||
def search_entity_view(username, entity):
|
||||
kind = 'user'
|
||||
avatar_data = avatar.get_data_for_user(entity)
|
||||
href = '/user/' + entity.username
|
||||
|
||||
if entity.organization:
|
||||
kind = 'organization'
|
||||
avatar_data = avatar.get_data_for_org(entity)
|
||||
href = '/organization/' + entity.username
|
||||
elif entity.robot:
|
||||
parts = parse_robot_username(entity.username)
|
||||
if parts[0] == username:
|
||||
href = '/user/' + username + '?tab=robots&showRobot=' + entity.username
|
||||
else:
|
||||
href = '/organization/' + parts[0] + '?tab=robots&showRobot=' + entity.username
|
||||
|
||||
kind = 'robot'
|
||||
avatar_data = None
|
||||
|
||||
return {
|
||||
'kind': kind,
|
||||
'avatar': avatar_data,
|
||||
'name': entity.username,
|
||||
'score': 1,
|
||||
'href': href
|
||||
}
|
||||
|
||||
|
||||
def conduct_team_search(username, query, encountered_teams, results):
|
||||
""" Finds the matching teams where the user is a member. """
|
||||
matching_teams = model.get_matching_user_teams(query, get_authenticated_user(), limit=5)
|
||||
for team in matching_teams:
|
||||
if team.id in encountered_teams:
|
||||
continue
|
||||
|
||||
encountered_teams.add(team.id)
|
||||
|
||||
results.append({
|
||||
'kind': 'team',
|
||||
'name': team.name,
|
||||
'organization': search_entity_view(username, team.organization),
|
||||
'avatar': avatar.get_data_for_team(team),
|
||||
'score': 2,
|
||||
'href': '/organization/' + team.organization.username + '/teams/' + team.name
|
||||
})
|
||||
|
||||
|
||||
def conduct_admined_team_search(username, query, encountered_teams, results):
|
||||
""" Finds matching teams in orgs admined by the user. """
|
||||
matching_teams = model.get_matching_admined_teams(query, get_authenticated_user(), limit=5)
|
||||
for team in matching_teams:
|
||||
if team.id in encountered_teams:
|
||||
continue
|
||||
|
||||
encountered_teams.add(team.id)
|
||||
|
||||
results.append({
|
||||
'kind': 'team',
|
||||
'name': team.name,
|
||||
'organization': search_entity_view(username, team.organization),
|
||||
'avatar': avatar.get_data_for_team(team),
|
||||
'score': 2,
|
||||
'href': '/organization/' + team.organization.username + '/teams/' + team.name
|
||||
})
|
||||
|
||||
|
||||
def conduct_repo_search(username, query, results):
|
||||
""" Finds matching repositories. """
|
||||
def can_read(repository):
|
||||
if repository.is_public:
|
||||
return True
|
||||
|
||||
return ReadRepositoryPermission(repository.namespace_user.username, repository.name).can()
|
||||
|
||||
only_public = username is None
|
||||
matching_repos = model.get_sorted_matching_repositories(query, only_public, can_read, limit=5)
|
||||
|
||||
for repo in matching_repos:
|
||||
repo_score = math.log(repo.count or 1, 10) or 1
|
||||
|
||||
# If the repository is under the user's namespace, give it 20% more weight.
|
||||
namespace = repo.namespace_user.username
|
||||
if OrganizationMemberPermission(namespace).can() or namespace == username:
|
||||
repo_score = repo_score * 1.2
|
||||
|
||||
results.append({
|
||||
'kind': 'repository',
|
||||
'namespace': search_entity_view(username, repo.namespace_user),
|
||||
'name': repo.name,
|
||||
'description': repo.description,
|
||||
'is_public': repo.is_public,
|
||||
'score': repo_score,
|
||||
'href': '/repository/' + repo.namespace_user.username + '/' + repo.name
|
||||
})
|
||||
|
||||
|
||||
def conduct_namespace_search(username, query, results):
|
||||
""" Finds matching users and organizations. """
|
||||
matching_entities = model.get_matching_user_namespaces(query, username, limit=5)
|
||||
for entity in matching_entities:
|
||||
results.append(search_entity_view(username, entity))
|
||||
|
||||
|
||||
def conduct_robot_search(username, query, results):
|
||||
""" Finds matching robot accounts. """
|
||||
matching_robots = model.get_matching_robots(query, username, limit=5)
|
||||
for robot in matching_robots:
|
||||
results.append(search_entity_view(username, robot))
|
||||
|
||||
|
||||
@resource('/v1/find/all')
|
||||
class ConductSearch(ApiResource):
|
||||
""" Resource for finding users, repositories, teams, etc. """
|
||||
@parse_args
|
||||
@query_param('query', 'The search query.', type=str, default='')
|
||||
@require_scope(scopes.READ_REPO)
|
||||
@nickname('conductSearch')
|
||||
def get(self, args):
|
||||
""" Get a list of entities and resources that match the specified query. """
|
||||
query = args['query']
|
||||
if not query:
|
||||
return {'results': []}
|
||||
|
||||
username = None
|
||||
results = []
|
||||
|
||||
if get_authenticated_user():
|
||||
username = get_authenticated_user().username
|
||||
|
||||
# Search for teams.
|
||||
encountered_teams = set()
|
||||
conduct_team_search(username, query, encountered_teams, results)
|
||||
conduct_admined_team_search(username, query, encountered_teams, results)
|
||||
|
||||
# Search for robot accounts.
|
||||
conduct_robot_search(username, query, results)
|
||||
|
||||
# Search for repos.
|
||||
conduct_repo_search(username, query, results)
|
||||
|
||||
# Search for users and orgs.
|
||||
conduct_namespace_search(username, query, results)
|
||||
|
||||
# Modify the results' scores via how close the query term is to each result's name.
|
||||
for result in results:
|
||||
result['score'] = result['score'] * liquidmetal.score(result['name'], query)
|
||||
|
||||
return {'results': sorted(results, key=itemgetter('score'), reverse=True)}
|
||||
|
|
|
@ -15,6 +15,7 @@ from auth.permissions import SuperUserPermission
|
|||
from auth.auth_context import get_authenticated_user
|
||||
from data.database import User
|
||||
from util.config.configutil import add_enterprise_config_defaults
|
||||
from util.config.provider import CannotWriteConfigException
|
||||
from util.config.validator import validate_service_for_config, SSL_FILENAMES
|
||||
from data.runmigration import run_alembic_migration
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ def user_view(user):
|
|||
'username': user.username,
|
||||
'email': user.email,
|
||||
'verified': user.verified,
|
||||
'avatar': avatar.compute_hash(user.email, name=user.username),
|
||||
'avatar': avatar.get_data_for_user(user),
|
||||
'super_user': superusers.is_superuser(user.username)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,45 @@
|
|||
from flask import request
|
||||
from flask import request, abort
|
||||
|
||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||
RepositoryParamResource, log_action, NotFound, validate_json_request,
|
||||
path_param)
|
||||
path_param, format_date)
|
||||
from endpoints.api.image import image_view
|
||||
from data import model
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class ListRepositoryTags(RepositoryParamResource):
|
||||
""" Resource for listing repository tags. """
|
||||
|
||||
@require_repo_write
|
||||
@nickname('listRepoTags')
|
||||
def get(self, namespace, repository):
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if not repo:
|
||||
abort(404)
|
||||
|
||||
def tag_view(tag):
|
||||
tag_info = {
|
||||
'name': tag.name,
|
||||
'docker_image_id': tag.image.docker_image_id,
|
||||
}
|
||||
|
||||
if tag.lifetime_start_ts > 0:
|
||||
tag_info['start_ts'] = tag.lifetime_start_ts
|
||||
|
||||
if tag.lifetime_end_ts > 0:
|
||||
tag_info['end_ts'] = tag.lifetime_end_ts
|
||||
|
||||
return tag_info
|
||||
|
||||
tags = model.list_repository_tag_history(repo, limit=100)
|
||||
return {'tags': [tag_view(tag) for tag in tags]}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
|
@ -92,7 +125,7 @@ class RepositoryTagImages(RepositoryParamResource):
|
|||
parent_images = model.get_parent_images(namespace, repository, tag_image)
|
||||
image_map = {}
|
||||
for image in parent_images:
|
||||
image_map[str(image.id)] = image.docker_image_id
|
||||
image_map[str(image.id)] = image
|
||||
|
||||
parents = list(parent_images)
|
||||
parents.reverse()
|
||||
|
|
|
@ -52,11 +52,11 @@ def team_view(orgname, team):
|
|||
view_permission = ViewTeamPermission(orgname, team.name)
|
||||
role = model.get_team_org_role(team).name
|
||||
return {
|
||||
'id': team.id,
|
||||
'name': team.name,
|
||||
'description': team.description,
|
||||
'can_view': view_permission.can(),
|
||||
'role': role
|
||||
'role': role,
|
||||
'avatar': avatar.get_data_for_team(team)
|
||||
}
|
||||
|
||||
def member_view(member, invited=False):
|
||||
|
@ -64,7 +64,7 @@ def member_view(member, invited=False):
|
|||
'name': member.username,
|
||||
'kind': 'user',
|
||||
'is_robot': member.robot,
|
||||
'avatar': avatar.compute_hash(member.email, name=member.username) if not member.robot else None,
|
||||
'avatar': avatar.get_data_for_user(member),
|
||||
'invited': invited,
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ def invite_view(invite):
|
|||
return {
|
||||
'email': invite.email,
|
||||
'kind': 'invite',
|
||||
'avatar': avatar.compute_hash(invite.email),
|
||||
'avatar': avatar.get_data(invite.email, invite.email, 'user'),
|
||||
'invited': True
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import logging
|
||||
import json
|
||||
|
||||
from flask import request
|
||||
from random import SystemRandom
|
||||
from flask import request, abort
|
||||
from flask.ext.login import logout_user
|
||||
from flask.ext.principal import identity_changed, AnonymousIdentity
|
||||
from peewee import IntegrityError
|
||||
|
@ -35,7 +36,7 @@ def user_view(user):
|
|||
admin_org = AdministerOrganizationPermission(o.username)
|
||||
return {
|
||||
'name': o.username,
|
||||
'avatar': avatar.compute_hash(o.email, name=o.username),
|
||||
'avatar': avatar.get_data_for_org(o),
|
||||
'is_org_admin': admin_org.can(),
|
||||
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
|
||||
'preferred_namespace': not (o.stripe_id is None)
|
||||
|
@ -58,15 +59,16 @@ def user_view(user):
|
|||
logins = model.list_federated_logins(user)
|
||||
|
||||
user_response = {
|
||||
'verified': user.verified,
|
||||
'anonymous': False,
|
||||
'username': user.username,
|
||||
'avatar': avatar.compute_hash(user.email, name=user.username),
|
||||
'avatar': avatar.get_data_for_user(user)
|
||||
}
|
||||
|
||||
user_admin = UserAdminPermission(user.username)
|
||||
if user_admin.can():
|
||||
user_response.update({
|
||||
'is_me': True,
|
||||
'verified': user.verified,
|
||||
'email': user.email,
|
||||
'organizations': [org_view(o) for o in organizations],
|
||||
'logins': [login_view(login) for login in logins],
|
||||
|
@ -76,7 +78,7 @@ def user_view(user):
|
|||
'tag_expiration': user.removed_tag_expiration_s,
|
||||
})
|
||||
|
||||
if features.SUPER_USERS:
|
||||
if features.SUPER_USERS and SuperUserPermission().can():
|
||||
user_response.update({
|
||||
'super_user': user and user == get_authenticated_user() and SuperUserPermission().can()
|
||||
})
|
||||
|
@ -175,8 +177,8 @@ class User(ApiResource):
|
|||
'description': 'The user\'s email address',
|
||||
},
|
||||
'avatar': {
|
||||
'type': 'string',
|
||||
'description': 'Avatar hash representing the user\'s icon'
|
||||
'type': 'object',
|
||||
'description': 'Avatar data representing the user\'s icon'
|
||||
},
|
||||
'organizations': {
|
||||
'type': 'array',
|
||||
|
@ -224,8 +226,13 @@ class User(ApiResource):
|
|||
if 'password' in user_data:
|
||||
logger.debug('Changing password for user: %s', user.username)
|
||||
log_action('account_change_password', user.username)
|
||||
|
||||
# Change the user's password.
|
||||
model.change_password(user, user_data['password'])
|
||||
|
||||
# Login again to reset their session cookie.
|
||||
common_login(user)
|
||||
|
||||
if features.MAILING:
|
||||
send_password_changed(user.username, user.email)
|
||||
|
||||
|
@ -335,13 +342,51 @@ class PrivateRepositories(ApiResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/user/clientkey')
|
||||
@internal_only
|
||||
class ClientKey(ApiResource):
|
||||
""" Operations for returning an encrypted key which can be used in place of a password
|
||||
for the Docker client. """
|
||||
schemas = {
|
||||
'GenerateClientKey': {
|
||||
'id': 'GenerateClientKey',
|
||||
'type': 'object',
|
||||
'required': [
|
||||
'password',
|
||||
],
|
||||
'properties': {
|
||||
'password': {
|
||||
'type': 'string',
|
||||
'description': 'The user\'s password',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
@nickname('generateUserClientKey')
|
||||
@validate_json_request('GenerateClientKey')
|
||||
def post(self):
|
||||
""" Return's the user's private client key. """
|
||||
username = get_authenticated_user().username
|
||||
password = request.get_json()['password']
|
||||
|
||||
(result, error_message) = authentication.verify_user(username, password)
|
||||
if not result:
|
||||
raise request_error(message=error_message)
|
||||
|
||||
return {
|
||||
'key': authentication.encrypt_user_password(password)
|
||||
}
|
||||
|
||||
|
||||
def conduct_signin(username_or_email, password):
|
||||
needs_email_verification = False
|
||||
invalid_credentials = False
|
||||
|
||||
verified = None
|
||||
try:
|
||||
verified = authentication.verify_user(username_or_email, password)
|
||||
(verified, error_message) = authentication.verify_user(username_or_email, password)
|
||||
except model.TooManyUsersException as ex:
|
||||
raise license_error(exception=ex)
|
||||
|
||||
|
@ -407,7 +452,7 @@ class ConvertToOrganization(ApiResource):
|
|||
|
||||
# Ensure that the sign in credentials work.
|
||||
admin_password = convert_data['adminPassword']
|
||||
admin_user = authentication.verify_user(admin_username, admin_password)
|
||||
(admin_user, error_message) = authentication.verify_user(admin_username, admin_password)
|
||||
if not admin_user:
|
||||
raise request_error(reason='invaliduser',
|
||||
message='The admin user credentials are not valid')
|
||||
|
@ -621,17 +666,16 @@ class UserNotification(ApiResource):
|
|||
|
||||
def authorization_view(access_token):
|
||||
oauth_app = access_token.application
|
||||
app_email = oauth_app.avatar_email or oauth_app.organization.email
|
||||
return {
|
||||
'application': {
|
||||
'name': oauth_app.name,
|
||||
'description': oauth_app.description,
|
||||
'url': oauth_app.application_uri,
|
||||
'avatar': avatar.compute_hash(oauth_app.avatar_email or oauth_app.organization.email,
|
||||
name=oauth_app.name),
|
||||
'avatar': avatar.get_data(oauth_app.name, app_email, 'app'),
|
||||
'organization': {
|
||||
'name': oauth_app.organization.username,
|
||||
'avatar': avatar.compute_hash(oauth_app.organization.email,
|
||||
name=oauth_app.organization.username)
|
||||
'avatar': avatar.get_data_for_org(oauth_app.organization)
|
||||
}
|
||||
},
|
||||
'scopes': scopes.get_scope_information(access_token.scope),
|
||||
|
@ -745,6 +789,7 @@ class StarredRepositoryList(ApiResource):
|
|||
'repository': repository,
|
||||
}, 201
|
||||
|
||||
|
||||
@resource('/v1/user/starred/<repopath:repository>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class StarredRepository(RepositoryParamResource):
|
||||
|
@ -759,3 +804,17 @@ class StarredRepository(RepositoryParamResource):
|
|||
if repo:
|
||||
model.unstar_repository(user, repo)
|
||||
return 'Deleted', 204
|
||||
|
||||
|
||||
@resource('/v1/users/<username>')
|
||||
class Users(ApiResource):
|
||||
""" Operations related to retrieving information about other users. """
|
||||
@nickname('getUserInformation')
|
||||
def get(self, username):
|
||||
""" Get user information for the specified user. """
|
||||
user = model.get_user(username)
|
||||
if user is None or user.organization or user.robot:
|
||||
abort(404)
|
||||
|
||||
return user_view(user)
|
||||
|
||||
|
|
|
@ -157,7 +157,10 @@ def github_oauth_callback():
|
|||
if error:
|
||||
return render_ologin_error('GitHub', error)
|
||||
|
||||
# Exchange the OAuth code.
|
||||
token = exchange_code_for_token(request.args.get('code'), github_login)
|
||||
|
||||
# Retrieve the user's information.
|
||||
user_data = get_user(github_login, token)
|
||||
if not user_data or not 'login' in user_data:
|
||||
return render_ologin_error('GitHub')
|
||||
|
@ -172,16 +175,35 @@ def github_oauth_callback():
|
|||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
|
||||
# Retrieve the user's orgnizations (if organization filtering is turned on)
|
||||
if github_login.allowed_organizations() is not None:
|
||||
get_orgs = client.get(github_login.orgs_endpoint(), params=token_param,
|
||||
headers={'Accept': 'application/vnd.github.moondragon+json'})
|
||||
|
||||
organizations = set([org.get('login').lower() for org in get_orgs.json()])
|
||||
if not (organizations & set(github_login.allowed_organizations())):
|
||||
err = """You are not a member of an allowed GitHub organization.
|
||||
Please contact your system administrator if you believe this is in error."""
|
||||
return render_ologin_error('GitHub', err)
|
||||
|
||||
# Find the e-mail address for the user: we will accept any email, but we prefer the primary
|
||||
get_email = client.get(github_login.email_endpoint(), params=token_param,
|
||||
headers=v3_media_type)
|
||||
|
||||
# We will accept any email, but we prefer the primary
|
||||
found_email = None
|
||||
for user_email in get_email.json():
|
||||
if not github_login.is_enterprise() and not user_email['verified']:
|
||||
continue
|
||||
|
||||
found_email = user_email['email']
|
||||
if user_email['primary']:
|
||||
break
|
||||
|
||||
if found_email is None:
|
||||
err = 'There is no verified e-mail address attached to the GitHub account.'
|
||||
return render_ologin_error('GitHub', err)
|
||||
|
||||
metadata = {
|
||||
'service_username': username
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import json
|
|||
from flask import make_response
|
||||
from app import app
|
||||
from util.useremails import CannotSendEmailException
|
||||
from util.config.provider import CannotWriteConfigException
|
||||
from data import model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -17,3 +18,11 @@ def handle_dme(ex):
|
|||
def handle_emailexception(ex):
|
||||
message = 'Could not send email. Please contact an administrator and report this problem.'
|
||||
return make_response(json.dumps({'message': message}), 400)
|
||||
|
||||
@app.errorhandler(CannotWriteConfigException)
|
||||
def handle_configexception(ex):
|
||||
message = ('Configuration could not be written to the mounted volume. \n' +
|
||||
'Please make sure the mounted volume is not read-only and restart ' +
|
||||
'the setup process. \n\nIssue: %s' % ex)
|
||||
|
||||
return make_response(json.dumps({'message': message}), 400)
|
|
@ -109,19 +109,17 @@ def create_user():
|
|||
issue='robot-login-failure')
|
||||
|
||||
if authentication.user_exists(username):
|
||||
verified = authentication.verify_user(username, password)
|
||||
(verified, error_message) = authentication.verify_user(username, password, basic_auth=True)
|
||||
if verified:
|
||||
# Mark that the user was logged in.
|
||||
event = userevents.get_event(username)
|
||||
event.publish_event_data('docker-cli', {'action': 'login'})
|
||||
|
||||
return success
|
||||
else:
|
||||
# Mark that the login failed.
|
||||
event = userevents.get_event(username)
|
||||
event.publish_event_data('docker-cli', {'action': 'loginfailure'})
|
||||
|
||||
abort(400, 'Invalid password.', issue='login-failure')
|
||||
abort(400, error_message, issue='login-failure')
|
||||
|
||||
elif not features.USER_CREATION:
|
||||
abort(400, 'User creation is disabled. Please speak to your administrator.')
|
||||
|
@ -231,6 +229,16 @@ def create_repository(namespace, repository):
|
|||
repo = model.create_repository(namespace, repository,
|
||||
get_authenticated_user())
|
||||
|
||||
if get_authenticated_user():
|
||||
user_event_data = {
|
||||
'action': 'push_start',
|
||||
'repository': repository,
|
||||
'namespace': namespace
|
||||
}
|
||||
|
||||
event = userevents.get_event(get_authenticated_user().username)
|
||||
event.publish_event_data('docker-cli', user_event_data)
|
||||
|
||||
return make_response('Created', 201)
|
||||
|
||||
|
||||
|
@ -248,20 +256,6 @@ def update_images(namespace, repository):
|
|||
# Make sure the repo actually exists.
|
||||
abort(404, message='Unknown repository', issue='unknown-repo')
|
||||
|
||||
if get_authenticated_user():
|
||||
logger.debug('Publishing push event')
|
||||
username = get_authenticated_user().username
|
||||
|
||||
# Mark that the user has pushed the repo.
|
||||
user_data = {
|
||||
'action': 'pushed_repo',
|
||||
'repository': repository,
|
||||
'namespace': namespace
|
||||
}
|
||||
|
||||
event = userevents.get_event(username)
|
||||
event.publish_event_data('docker-cli', user_data)
|
||||
|
||||
logger.debug('GCing repository')
|
||||
model.garbage_collect_repository(namespace, repository)
|
||||
|
||||
|
@ -272,6 +266,7 @@ def update_images(namespace, repository):
|
|||
event_data = {
|
||||
'updated_tags': updated_tags,
|
||||
}
|
||||
|
||||
track_and_log('push_repo', repo)
|
||||
spawn_notification(repo, 'repo_push', event_data)
|
||||
return make_response('Updated', 204)
|
||||
|
|
|
@ -3,7 +3,6 @@ import logging
|
|||
from flask import (abort, redirect, request, url_for, make_response, Response,
|
||||
Blueprint, send_from_directory, jsonify, send_file)
|
||||
|
||||
from avatar_generator import Avatar
|
||||
from flask.ext.login import current_user
|
||||
from urlparse import urlparse
|
||||
from health.healthcheck import get_healthchecker
|
||||
|
@ -39,7 +38,6 @@ STATUS_TAGS = app.config['STATUS_TAGS']
|
|||
|
||||
|
||||
@web.route('/', methods=['GET'], defaults={'path': ''})
|
||||
@web.route('/organization/<path:path>', methods=['GET'])
|
||||
@no_cache
|
||||
def index(path, **kwargs):
|
||||
return render_page_template('index.html', **kwargs)
|
||||
|
@ -50,6 +48,18 @@ def internal_error_display():
|
|||
return render_page_template('500.html')
|
||||
|
||||
|
||||
@web.route('/organization/<path:path>', methods=['GET'])
|
||||
@no_cache
|
||||
def org_view(path):
|
||||
return index('')
|
||||
|
||||
|
||||
@web.route('/user/<path:path>', methods=['GET'])
|
||||
@no_cache
|
||||
def user_view(path):
|
||||
return index('')
|
||||
|
||||
|
||||
@web.route('/snapshot', methods=['GET'])
|
||||
@web.route('/snapshot/', methods=['GET'])
|
||||
@web.route('/snapshot/<path:path>', methods=['GET'])
|
||||
|
@ -210,20 +220,6 @@ def endtoend_health():
|
|||
return response
|
||||
|
||||
|
||||
@app.route("/avatar/<avatar_hash>")
|
||||
@set_cache_headers
|
||||
def render_avatar(avatar_hash, headers):
|
||||
try:
|
||||
size = int(request.args.get('size', 16))
|
||||
except ValueError:
|
||||
size = 16
|
||||
|
||||
generated = Avatar.generate(size, avatar_hash, "PNG")
|
||||
resp = make_response(generated, 200, {'Content-Type': 'image/png'})
|
||||
resp.headers.extend(headers)
|
||||
return resp
|
||||
|
||||
|
||||
@web.route('/tos', methods=['GET'])
|
||||
@no_cache
|
||||
def tos():
|
||||
|
@ -248,21 +244,6 @@ def robots():
|
|||
return send_from_directory('static', 'robots.txt')
|
||||
|
||||
|
||||
@web.route('/<path:repository>')
|
||||
@no_cache
|
||||
@process_oauth
|
||||
@parse_repository_name_and_tag
|
||||
def redirect_to_repository(namespace, reponame, tag):
|
||||
permission = ReadRepositoryPermission(namespace, reponame)
|
||||
is_public = model.repository_is_public(namespace, reponame)
|
||||
|
||||
if permission.can() or is_public:
|
||||
repository_name = '/'.join([namespace, reponame])
|
||||
return redirect(url_for('web.repository', path=repository_name, tag=tag))
|
||||
|
||||
abort(404)
|
||||
|
||||
|
||||
@web.route('/receipt', methods=['GET'])
|
||||
@route_show_if(features.BILLING)
|
||||
@require_session_login
|
||||
|
@ -449,15 +430,16 @@ def request_authorization_code():
|
|||
|
||||
# Load the application information.
|
||||
oauth_app = provider.get_application_for_client_id(client_id)
|
||||
app_email = oauth_app.email or organization.email
|
||||
|
||||
oauth_app_view = {
|
||||
'name': oauth_app.name,
|
||||
'description': oauth_app.description,
|
||||
'url': oauth_app.application_uri,
|
||||
'avatar': avatar.compute_hash(oauth_app.avatar_email, name=oauth_app.name),
|
||||
'avatar': avatar.get_data(oauth_app.name, app_email, 'app'),
|
||||
'organization': {
|
||||
'name': oauth_app.organization.username,
|
||||
'avatar': avatar.compute_hash(oauth_app.organization.email,
|
||||
name=oauth_app.organization.username)
|
||||
'avatar': avatar.get_data_for_org(oauth_app.organization)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -533,3 +515,31 @@ def attach_custom_build_trigger(namespace, repository_name):
|
|||
return redirect(full_url)
|
||||
|
||||
abort(403)
|
||||
|
||||
@web.route('/<path:repository>')
|
||||
@no_cache
|
||||
@process_oauth
|
||||
@parse_repository_name_and_tag
|
||||
def redirect_to_repository(namespace, reponame, tag):
|
||||
permission = ReadRepositoryPermission(namespace, reponame)
|
||||
is_public = model.repository_is_public(namespace, reponame)
|
||||
|
||||
if permission.can() or is_public:
|
||||
repository_name = '/'.join([namespace, reponame])
|
||||
return redirect(url_for('web.repository', path=repository_name, tag=tag))
|
||||
|
||||
abort(404)
|
||||
|
||||
|
||||
@web.route('/<namespace>')
|
||||
@no_cache
|
||||
@process_oauth
|
||||
def redirect_to_namespace(namespace):
|
||||
user_or_org = model.get_user_or_org(namespace)
|
||||
if not user_or_org:
|
||||
abort(404)
|
||||
|
||||
if user_or_org.organization:
|
||||
return redirect(url_for('web.org_view', path=namespace))
|
||||
else:
|
||||
return redirect(url_for('web.user_view', path=namespace))
|
||||
|
|
|
@ -21,12 +21,17 @@ EXTERNAL_CSS = [
|
|||
'netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.css',
|
||||
'netdna.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css',
|
||||
'fonts.googleapis.com/css?family=Source+Sans+Pro:400,700',
|
||||
's3.amazonaws.com/cdn.core-os.net/icons/core-icons.css'
|
||||
]
|
||||
|
||||
EXTERNAL_FONTS = [
|
||||
'netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.woff?v=4.2.0',
|
||||
'netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.ttf?v=4.2.0',
|
||||
'netdna.bootstrapcdn.com/font-awesome/4.2.0/fonts/fontawesome-webfont.svg?v=4.2.0',
|
||||
|
||||
'cdn.core-os.net/icons/core-icons.woff',
|
||||
'cdn.core-os.net/icons/core-icons.ttf',
|
||||
'cdn.core-os.net/icons/core-icons.svg',
|
||||
]
|
||||
|
||||
|
||||
|
|
16
initdb.py
|
@ -57,7 +57,7 @@ def __gen_image_uuid(repo, image_num):
|
|||
|
||||
|
||||
global_image_num = [0]
|
||||
def __create_subtree(repo, structure, creator_username, parent):
|
||||
def __create_subtree(repo, structure, creator_username, parent, tag_map):
|
||||
num_nodes, subtrees, last_node_tags = structure
|
||||
|
||||
# create the nodes
|
||||
|
@ -102,12 +102,18 @@ def __create_subtree(repo, structure, creator_username, parent):
|
|||
tag = model.create_or_update_tag(repo.namespace_user.username, repo.name, tag_name,
|
||||
new_image.docker_image_id)
|
||||
|
||||
tag_map[tag_name] = tag
|
||||
|
||||
for tag_name in last_node_tags:
|
||||
if tag_name[0] == '#':
|
||||
tag.lifetime_end_ts = int(time.time()) - 1
|
||||
tag = tag_map[tag_name]
|
||||
tag.name = tag_name[1:]
|
||||
tag.lifetime_end_ts = tag_map[tag_name[1:]].lifetime_start_ts
|
||||
tag.lifetime_start_ts = tag.lifetime_end_ts - 10
|
||||
tag.save()
|
||||
|
||||
for subtree in subtrees:
|
||||
__create_subtree(repo, subtree, creator_username, new_image)
|
||||
__create_subtree(repo, subtree, creator_username, new_image, tag_map)
|
||||
|
||||
|
||||
def __generate_repository(user, name, description, is_public, permissions,
|
||||
|
@ -127,9 +133,9 @@ def __generate_repository(user, name, description, is_public, permissions,
|
|||
|
||||
if isinstance(structure, list):
|
||||
for s in structure:
|
||||
__create_subtree(repo, s, user.username, None)
|
||||
__create_subtree(repo, s, user.username, None, {})
|
||||
else:
|
||||
__create_subtree(repo, structure, user.username, None)
|
||||
__create_subtree(repo, structure, user.username, None, {})
|
||||
|
||||
return repo
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@ import logging.config
|
|||
|
||||
from app import app as application
|
||||
|
||||
# Note: We need to import this module to make sure the decorators are registered.
|
||||
import endpoints.decorated
|
||||
|
||||
from endpoints.index import index
|
||||
from endpoints.tags import tags
|
||||
from endpoints.registry import registry
|
||||
|
|
|
@ -48,3 +48,4 @@ pygpgme
|
|||
cachetools
|
||||
mock
|
||||
psutil
|
||||
stringscore
|
|
@ -54,6 +54,7 @@ redis==2.10.3
|
|||
reportlab==2.7
|
||||
requests==2.5.1
|
||||
six==1.9.0
|
||||
stringscore==0.1.0
|
||||
stripe==1.20.1
|
||||
trollius==1.0.4
|
||||
tzlocal==1.1.2
|
||||
|
|
|
@ -278,6 +278,15 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.config-list-field-element input {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.config-list-field-element .item-delete {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.config-list-field-element input {
|
||||
width: 350px;
|
||||
}
|
||||
|
@ -764,10 +773,17 @@
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
.co-table.no-lines td {
|
||||
border-bottom: 0px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.co-table thead td {
|
||||
color: #999;
|
||||
font-size: 90%;
|
||||
text-transform: uppercase;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
font-weight: 300;
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
|
||||
.co-table thead td a {
|
||||
|
@ -804,11 +820,45 @@
|
|||
width: 30px;
|
||||
}
|
||||
|
||||
.co-table td.caret-col {
|
||||
width: 10px;
|
||||
padding-left: 6px;
|
||||
padding-right: 0px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.co-table td.caret-col i.fa {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.co-table .add-row-spacer td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.co-table .add-row td {
|
||||
padding-top: 10px;
|
||||
border-top: 2px solid #eee;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.co-table tr.co-table-header-row td {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: #ccc;
|
||||
border-bottom: none;
|
||||
padding-left: 10px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.co-table tr.co-table-header-row td i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.co-table tr.indented-row td:first-child {
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.cor-checkable-menu {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -910,3 +960,78 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.co-alert {
|
||||
padding: 16px;
|
||||
padding-left: 46px;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-success {
|
||||
background: #F0FFF4;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-success:before {
|
||||
font-family: FontAwesome;
|
||||
content: "\f058";
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 12px;
|
||||
font-size: 22px;
|
||||
color: #83D29C;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-info {
|
||||
background: #F0FAFF;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-info:before {
|
||||
font-family: FontAwesome;
|
||||
content: "\f05a";
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 12px;
|
||||
font-size: 22px;
|
||||
color: #83B7D2;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-warning {
|
||||
background: #FFFBF0;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-warning:before {
|
||||
font-family: FontAwesome;
|
||||
content: "\f071";
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 12px;
|
||||
font-size: 22px;
|
||||
color: #E4C212;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-danger {
|
||||
background: #FFF0F0;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-danger:before {
|
||||
font-family: core-icons;
|
||||
content: "\f107";
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 12px;
|
||||
font-size: 22px;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.co-alert.co-alert-danger:after {
|
||||
font-family: FontAwesome;
|
||||
content: "\f12a";
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 20px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
z-index: 2;
|
||||
}
|
|
@ -1,3 +1,14 @@
|
|||
.repo-panel-info-element .right-controls {
|
||||
margin-bottom: 20px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.repo-panel-info-element .right-controls .copy-box {
|
||||
width: 400px;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.repo-panel-info-element .stat-col {
|
||||
border-right: 2px solid #eee;
|
||||
}
|
||||
|
|
|
@ -63,4 +63,136 @@
|
|||
|
||||
.repo-panel-tags-element .options-col {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .options-col .fa-download {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-list {
|
||||
margin: 10px;
|
||||
border-left: 2px solid #eee;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry {
|
||||
position:relative;
|
||||
margin-top: 20px;
|
||||
padding-left: 26px;
|
||||
|
||||
transition: all 350ms ease-in-out;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-text {
|
||||
transition: transform 350ms ease-in-out, opacity 350ms ease-in-out;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.filtered-mismatch {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.filtered-mismatch .history-text {
|
||||
height: 18px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.filtered-mismatch .history-icon {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.5, 0.5);
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-date-break {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-date-break:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #ccc;
|
||||
top: 4px;
|
||||
left: -7px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-icon {
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 33px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
background: #ccc;
|
||||
|
||||
position: absolute;
|
||||
left: -17px;
|
||||
top: -4px;
|
||||
display: inline-block;
|
||||
|
||||
transition: all 350ms ease-in-out;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.move .history-icon:before {
|
||||
content: "\f061";
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.create .history-icon:before {
|
||||
content: "\f02b";
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.delete .history-icon:before {
|
||||
content: "\f014";
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.move .history-icon {
|
||||
background-color: #1f77b4;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.create .history-icon {
|
||||
background-color: #98df8a;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.delete .history-icon {
|
||||
background-color: #ff9896;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-icon .fa-tag {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .tag-span {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
background: #eee;
|
||||
padding-right: 6px;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .tag-span.checked {
|
||||
background: #F6FCFF;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .tag-span:before {
|
||||
content: "\f02b";
|
||||
font-family: FontAwesome;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-description {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-datetime {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
7
static/css/directives/ui/application-manager.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.application-manager-element .co-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.application-manager-element i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
3
static/css/directives/ui/authorized-apps-manager.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.authorized-apps-manager .avatar {
|
||||
margin-right: 4px;
|
||||
}
|
31
static/css/directives/ui/avatar.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
.avatar-element {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: white !important;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-element.team {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-element img {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.avatar-element .letter {
|
||||
cursor: default !important;
|
||||
font-style: normal !important;
|
||||
font-weight: normal !important;
|
||||
font-variant: normal !important;
|
||||
}
|
||||
|
||||
a .avatar-element .letter {
|
||||
cursor: pointer !important;
|
||||
}
|
25
static/css/directives/ui/billing-invoices.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
.billing-invoices-element .invoice-title {
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-status .success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-status .pending {
|
||||
color: steelblue;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-status .danger {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-amount:before {
|
||||
content: '$';
|
||||
}
|
||||
|
||||
.billing-invoices-element .fa-download {
|
||||
color: #aaa;
|
||||
}
|
|
@ -10,6 +10,11 @@
|
|||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.build-mini-status a {
|
||||
text-decoration: none !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.build-mini-status .timing {
|
||||
display: inline-block;
|
||||
margin-left: 30px;
|
||||
|
@ -29,5 +34,15 @@
|
|||
bottom: 4px;
|
||||
line-height: 33px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.build-mini-status .build-description .tbd-content {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
35
static/css/directives/ui/convert-user-to-org.css
Normal file
|
@ -0,0 +1,35 @@
|
|||
.convert-user-to-org .convert-form h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.convert-user-to-org #convertForm {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.convert-user-to-org #convertForm .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.convert-user-to-org #convertForm input {
|
||||
margin-bottom: 10px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.convert-user-to-org #convertForm .existing-data {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.convert-user-to-org #convertForm .description {
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.convert-user-to-org #convertForm .existing-data {
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
7
static/css/directives/ui/entity-reference.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.entity-reference .new-entity-reference .entity-name {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.entity-reference .new-entity-reference .fa-wrench {
|
||||
width: 16px;
|
||||
}
|
52
static/css/directives/ui/entity-search.css
Normal file
|
@ -0,0 +1,52 @@
|
|||
.entity-search-element {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.entity-search-element .entity-reference {
|
||||
position: absolute !important;
|
||||
top: 7px;
|
||||
left: 8px;
|
||||
right: 36px;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.entity-search-element .entity-reference .entity-reference-element {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.entity-search-element .entity-reference-element i.fa-exclamation-triangle {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.entity-search-element .entity-reference .entity-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.entity-search-element input {
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entity-search-element.persistent input {
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.entity-search-element .twitter-typeahead {
|
||||
vertical-align: middle;
|
||||
display: block !important;
|
||||
margin-right: 36px;
|
||||
}
|
||||
|
||||
.entity-search-element .dropdown {
|
||||
vertical-align: middle;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.entity-search-element .menuitem .avatar {
|
||||
margin-right: 4px;
|
||||
}
|
3
static/css/directives/ui/external-login-button.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.external-login-button i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
11
static/css/directives/ui/external-logins-manager.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.external-logins-manager .empty {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.external-logins-manager .external-auth-provider td:first-child {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.external-logins-manager .external-auth-provider td:first-child i.fa {
|
||||
margin-right: 6px;
|
||||
}
|
19
static/css/directives/ui/fetch-tag-dialog.css
Normal file
|
@ -0,0 +1,19 @@
|
|||
.fetch-tag-dialog .modal-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fetch-tag-dialog .modal-table .first-col {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.fetch-tag-dialog .co-dialog .modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.fetch-tag-dialog .entity-search {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.fetch-tag-dialog pre.command {
|
||||
margin-top: 10px;
|
||||
}
|
223
static/css/directives/ui/header-bar.css
Normal file
|
@ -0,0 +1,223 @@
|
|||
nav.navbar {
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-nav>li>a {
|
||||
letter-spacing: 0.5px;
|
||||
color: #428bca;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
nav.navbar-default .navbar-nav>li>a.active {
|
||||
color: #f04c5c;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>li>a:hover, .navbar-default .navbar-nav>li>a:focus {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:hover, .navbar-default .navbar-nav>.open>a:focus {
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
}
|
||||
|
||||
.header-bar-element .header-bar-content.search-visible {
|
||||
box-shadow: 0px 1px 4px #ccc;
|
||||
}
|
||||
|
||||
.header-bar-element .header-bar-content {
|
||||
z-index: 5;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: -50px;
|
||||
z-index: 4;
|
||||
height: 83px;
|
||||
transition: top 0.3s cubic-bezier(.23,.88,.72,.98);
|
||||
background: white;
|
||||
box-shadow: 0px 1px 16px #444;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box.search-visible {
|
||||
top: 50px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box.results-visible {
|
||||
box-shadow: 0px 1px 4px #ccc;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box .search-label {
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #ccc;
|
||||
margin-right: 10px;
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box .search-box-wrapper {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 100px;
|
||||
right: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-box .search-box-wrapper input {
|
||||
font-size: 28px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: -130px;
|
||||
z-index: 3;
|
||||
transition: top 0.4s cubic-bezier(.23,.88,.72,.98), height 0.25s ease-in-out;
|
||||
|
||||
background: white;
|
||||
box-shadow: 0px 1px 16px #444;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results.loading, .header-bar-element .search-results.results {
|
||||
top: 130px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results.loading {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results.no-results {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results ul {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li {
|
||||
list-style: none;
|
||||
padding: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .kind {
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
color: #aaa;
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results .avatar {
|
||||
margin-left: 6px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li.current {
|
||||
background: rgb(223, 242, 255);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li i.fa {
|
||||
margin-left: 6px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 24px;
|
||||
padding-left: 10px;
|
||||
display: inline-block;
|
||||
color: #aaa;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .score:before {
|
||||
content: "Score: ";
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .score {
|
||||
float: right;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .result-name {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-bar-element .search-results li .clarification {
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.header-bar-element .avatar {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.user-tools {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.user-tools .user-tool {
|
||||
font-size: 24px;
|
||||
margin-top: 14px;
|
||||
color: #428bca;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.user-tools.with-menu {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.user-tools .caret {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 23px
|
||||
}
|
||||
|
||||
.user-tools .notifications-bubble {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 13px;
|
||||
}
|
||||
|
||||
.user-tools i.user-tool:hover {
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-tools .new-menu {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.header-bar-element .context-dropdown i.fa {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
4
static/css/directives/ui/image-link.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.image-link a {
|
||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||
font-size: 12px;
|
||||
}
|
79
static/css/directives/ui/image-view-layer.css
Normal file
|
@ -0,0 +1,79 @@
|
|||
.image-view-layer-element {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
padding-left: 170px;
|
||||
}
|
||||
|
||||
.image-view-layer-element .image-id {
|
||||
font-family: monospace;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 110px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.image-view-layer-element .image-id a {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.image-view-layer-element.first .image-id {
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.image-view-layer-element.first .image-id a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.image-view-layer-element .image-comment {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.image-view-layer-element .nondocker-command {
|
||||
font-family: monospace;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.image-view-layer-element .nondocker-command:before {
|
||||
content: "\f120";
|
||||
font-family: "FontAwesome";
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.image-view-layer-element .image-layer-line {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 140px;
|
||||
|
||||
border-left: 2px solid #428bca;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.image-view-layer-element.first .image-layer-line {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.image-view-layer-element.last .image-layer-line {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.image-view-layer-element .image-layer-dot {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 135px;
|
||||
border: 2px solid #428bca;
|
||||
border-radius: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: white;
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.image-view-layer-element.first .image-layer-dot {
|
||||
background: #428bca;
|
||||
}
|
8
static/css/directives/ui/prototype-manager.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.prototype-manager-element i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.prototype-manager-element td {
|
||||
padding: 10px !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
|
@ -1,3 +1,15 @@
|
|||
.repository-permissions-table #add-entity-permission {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.repository-permissions-table .user-permission-entity {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repository-permissions-table .outside-org {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: -2px;
|
||||
font-size: 16px;
|
||||
color: #E8BB03;
|
||||
}
|
86
static/css/directives/ui/robots-manager.css
Normal file
|
@ -0,0 +1,86 @@
|
|||
.robots-manager-element .robot a {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.robots-manager-element .robot .prefix {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.robots-manager-element .robot i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.robots-manager-element .popup-input-button i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.robots-manager-element .empty {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.robots-manager-element tr.open td {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.robots-manager-element .permissions-table-wrapper {
|
||||
margin-left: 0px;
|
||||
border-left: 2px solid #ccc;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.robots-manager-element .permissions-table tbody tr:last-child td {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.robots-manager-element .permissions-display-row {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.robots-manager-element .permissions-display-row td:first-child {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle {
|
||||
color: #999;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #eee;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle .fa-hdd-o {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle.no-background .fa-hdd-o {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle .fa-lock {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle.no-background .fa-lock {
|
||||
bottom: 5px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.robots-manager-element .member-perm-summary {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.robots-manager-element .co-filter-box {
|
||||
float: right;
|
||||
min-width: 175px;
|
||||
margin-bottom: 10px;
|
||||
}
|
59
static/css/directives/ui/teams-manager.css
Normal file
|
@ -0,0 +1,59 @@
|
|||
.teams-manager .popup-input-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.teams-manager .manager-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.teams-manager .cor-options-menu {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.teams-manager .header-col .info-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.teams-manager .header-col .header-text {
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: #aaa !important;
|
||||
display: inline-block;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.teams-manager .control-col {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.teams-manager .team-listing {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.teams-manager .team-listing .avatar {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.teams-manager .team-member-list .fa {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.teams-manager .team-member-list {
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
padding: 4px;
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.teams-manager .team-member-list .team-member a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.teams-manager .team-member-list .team-member-more {
|
||||
vertical-align: middle;
|
||||
padding-left: 6px;
|
||||
color: #aaa;
|
||||
font-size: 14px;
|
||||
}
|
25
static/css/pages/image-view.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
.image-view .image-view-header {
|
||||
padding: 10px;
|
||||
background: #e8f1f6;
|
||||
margin: -10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.image-view .image-view-header .section-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.image-view .image-view-header .section {
|
||||
padding: 4px;
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.image-view .co-tab-content {
|
||||
padding: 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.image-view .co-tab-content h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
4
static/css/pages/new-organization.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.new-organization .co-main-content-panel {
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
}
|
90
static/css/pages/new-repo.css
Normal file
|
@ -0,0 +1,90 @@
|
|||
.new-repo .co-main-content-panel {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.new-repo .namespace-selector-header .slash {
|
||||
color: #444;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.new-repo .required-plan {
|
||||
margin: 10px;
|
||||
margin-top: 20px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.new-repo .required-plan .alert {
|
||||
color: #444 !important;
|
||||
}
|
||||
|
||||
.new-repo .new-header .popover {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.new-repo .new-header .repo-circle {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.new-repo .new-header .name-container {
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.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 .section-title {
|
||||
float: right;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.new-repo .repo-option {
|
||||
margin: 6px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.new-repo .repo-option label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.new-repo .initialize-repo .file-drop {
|
||||
margin: 10px;
|
||||
}
|
26
static/css/pages/org-view.css
Normal file
|
@ -0,0 +1,26 @@
|
|||
.org-view .organization-name {
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.org-view h3 {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.org-view .section-description-header {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.org-view .section-description-header:before {
|
||||
font-family: FontAwesome;
|
||||
content: "\f05a";
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 6px;
|
||||
font-size: 27px;
|
||||
color: #888;
|
||||
}
|
|
@ -24,3 +24,15 @@
|
|||
.repo-list .namespaces-list li .avatar {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.repo-list .new-org {
|
||||
margin-top: 20px !important;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.repo-list .new-org i.fa {
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
}
|
|
@ -44,3 +44,7 @@
|
|||
.repository-view .heading-controls .btn .fa {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.repository-view .tag-span {
|
||||
white-space: nowrap;
|
||||
}
|
55
static/css/pages/team-view.css
Normal file
|
@ -0,0 +1,55 @@
|
|||
.team-view .co-main-content-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.team-view .team-name {
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header button i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header > h3 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header .popover {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.team-view .team-view-header .popover.bottom-right .arrow:after {
|
||||
border-bottom-color: #f7f7f7;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header .popover-content {
|
||||
font-size: 14px;
|
||||
padding-top: 6px;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header .popover-content input {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.team-view .team-view-add-element .help-text {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.team-view .co-table-header-row td {
|
||||
padding-top: 20px !important;
|
||||
}
|
||||
|
||||
.team-view .co-table-header-row:first-child td {
|
||||
padding-top: 10px !important;
|
||||
}
|
3
static/css/pages/tutorial.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.tutorial-view .co-main-content-panel {
|
||||
padding: 30px;
|
||||
}
|