Merge remote-tracking branch 'bitbucket/master'
This commit is contained in:
commit
ed28bdb28b
87 changed files with 1677 additions and 158 deletions
|
@ -8,7 +8,7 @@ ENV HOME /root
|
||||||
RUN apt-get update # 10SEP2014
|
RUN apt-get update # 10SEP2014
|
||||||
|
|
||||||
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
# New ubuntu packages should be added as their own apt-get install lines below the existing install commands
|
||||||
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62 libjpeg62-dev libevent-2.0.5 libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap-2.4-2 libldap2-dev libsasl2-modules libsasl2-dev libpq5 libpq-dev
|
RUN apt-get install -y git python-virtualenv python-dev libjpeg8 libjpeg62 libjpeg62-dev libevent-2.0.5 libevent-dev gdebi-core g++ libmagic1 phantomjs nodejs npm libldap-2.4-2 libldap2-dev libsasl2-modules libsasl2-dev libpq5 libpq-dev libfreetype6-dev
|
||||||
|
|
||||||
# Build the python dependencies
|
# Build the python dependencies
|
||||||
ADD requirements.txt requirements.txt
|
ADD requirements.txt requirements.txt
|
||||||
|
@ -52,6 +52,7 @@ ADD conf/init/nginx /etc/service/nginx
|
||||||
ADD conf/init/diffsworker /etc/service/diffsworker
|
ADD conf/init/diffsworker /etc/service/diffsworker
|
||||||
ADD conf/init/notificationworker /etc/service/notificationworker
|
ADD conf/init/notificationworker /etc/service/notificationworker
|
||||||
ADD conf/init/buildlogsarchiver /etc/service/buildlogsarchiver
|
ADD conf/init/buildlogsarchiver /etc/service/buildlogsarchiver
|
||||||
|
ADD conf/init/buildmanager /etc/service/buildmanager
|
||||||
|
|
||||||
# Download any external libs.
|
# Download any external libs.
|
||||||
RUN mkdir static/fonts static/ldn
|
RUN mkdir static/fonts static/ldn
|
||||||
|
|
2
app.py
2
app.py
|
@ -25,6 +25,7 @@ from data.buildlogs import BuildLogs
|
||||||
from data.archivedlogs import LogArchive
|
from data.archivedlogs import LogArchive
|
||||||
from data.queue import WorkQueue
|
from data.queue import WorkQueue
|
||||||
from data.userevent import UserEventsBuilderModule
|
from data.userevent import UserEventsBuilderModule
|
||||||
|
from avatars.avatars import Avatar
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseConfig):
|
class Config(BaseConfig):
|
||||||
|
@ -119,6 +120,7 @@ features.import_features(app.config)
|
||||||
|
|
||||||
Principal(app, use_sessions=False)
|
Principal(app, use_sessions=False)
|
||||||
|
|
||||||
|
avatar = Avatar(app)
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
mail = Mail(app)
|
mail = Mail(app)
|
||||||
storage = Storage(app)
|
storage = Storage(app)
|
||||||
|
|
|
@ -76,7 +76,7 @@ IMPLIED_SCOPES = {
|
||||||
|
|
||||||
def scopes_from_scope_string(scopes):
|
def scopes_from_scope_string(scopes):
|
||||||
if not scopes:
|
if not scopes:
|
||||||
return {}
|
scopes = ''
|
||||||
|
|
||||||
return {ALL_SCOPES.get(scope, None) for scope in scopes.split(',')}
|
return {ALL_SCOPES.get(scope, None) for scope in scopes.split(',')}
|
||||||
|
|
||||||
|
|
0
avatars/__init__.py
Normal file
0
avatars/__init__.py
Normal file
65
avatars/avatars.py
Normal file
65
avatars/avatars.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
class Avatar(object):
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.app = app
|
||||||
|
self.state = self._init_app(app)
|
||||||
|
|
||||||
|
def _init_app(self, app):
|
||||||
|
return AVATAR_CLASSES[app.config.get('AVATAR_KIND', 'Gravatar')](
|
||||||
|
app.config['SERVER_HOSTNAME'],
|
||||||
|
app.config['PREFERRED_URL_SCHEME'])
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self.state, name, None)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAvatar(object):
|
||||||
|
""" Base class for all avatar implementations. """
|
||||||
|
def __init__(self, server_hostname, preferred_url_scheme):
|
||||||
|
self.server_hostname = server_hostname
|
||||||
|
self.preferred_url_scheme = preferred_url_scheme
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
AVATAR_CLASSES = {
|
||||||
|
'gravatar': GravatarAvatar,
|
||||||
|
'local': LocalAvatar
|
||||||
|
}
|
Binary file not shown.
0
buildman/__init__.py
Normal file
0
buildman/__init__.py
Normal file
56
buildman/builder.py
Normal file
56
buildman/builder.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import features
|
||||||
|
import time
|
||||||
|
|
||||||
|
from app import app, userfiles as user_files, build_logs, dockerfile_build_queue
|
||||||
|
|
||||||
|
from buildman.manager.enterprise import EnterpriseManager
|
||||||
|
from buildman.server import BuilderServer
|
||||||
|
|
||||||
|
from trollius import SSLContext
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BUILD_MANAGERS = {
|
||||||
|
'enterprise': EnterpriseManager
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTERNALLY_MANAGED = 'external'
|
||||||
|
|
||||||
|
def run_build_manager():
|
||||||
|
if not features.BUILD_SUPPORT:
|
||||||
|
logger.debug('Building is disabled. Please enable the feature flag')
|
||||||
|
return
|
||||||
|
|
||||||
|
build_manager_config = app.config.get('BUILD_MANAGER')
|
||||||
|
if build_manager_config is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# If the build system is externally managed, then we just sleep this process.
|
||||||
|
if build_manager_config[0] == EXTERNALLY_MANAGED:
|
||||||
|
logger.debug('Builds are externally managed.')
|
||||||
|
while True:
|
||||||
|
time.sleep(1000)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug('Asking to start build manager with lifecycle "%s"', build_manager_config[0])
|
||||||
|
manager_klass = BUILD_MANAGERS.get(build_manager_config[0])
|
||||||
|
if manager_klass is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug('Starting build manager with lifecycle "%s"', build_manager_config[0])
|
||||||
|
ssl_context = None
|
||||||
|
if os.environ.get('SSL_CONFIG'):
|
||||||
|
logger.debug('Loading SSL cert and key')
|
||||||
|
ssl_context = SSLContext()
|
||||||
|
ssl_context.load_cert_chain(os.environ.get('SSL_CONFIG') + '/ssl.cert',
|
||||||
|
os.environ.get('SSL_CONFIG') + '/ssl.key')
|
||||||
|
|
||||||
|
server = BuilderServer(app.config['SERVER_HOSTNAME'], dockerfile_build_queue, build_logs,
|
||||||
|
user_files, manager_klass)
|
||||||
|
server.run('0.0.0.0', ssl=ssl_context)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
run_build_manager()
|
0
buildman/component/__init__.py
Normal file
0
buildman/component/__init__.py
Normal file
10
buildman/component/basecomponent.py
Normal file
10
buildman/component/basecomponent.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from autobahn.asyncio.wamp import ApplicationSession
|
||||||
|
|
||||||
|
class BaseComponent(ApplicationSession):
|
||||||
|
""" Base class for all registered component sessions in the server. """
|
||||||
|
def __init__(self, config, **kwargs):
|
||||||
|
ApplicationSession.__init__(self, config)
|
||||||
|
self.server = None
|
||||||
|
self.parent_manager = None
|
||||||
|
self.build_logs = None
|
||||||
|
self.user_files = None
|
365
buildman/component/buildcomponent.py
Normal file
365
buildman/component/buildcomponent.py
Normal file
|
@ -0,0 +1,365 @@
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import trollius
|
||||||
|
import re
|
||||||
|
|
||||||
|
from autobahn.wamp.exception import ApplicationError
|
||||||
|
from trollius.coroutines import From
|
||||||
|
|
||||||
|
from buildman.server import BuildJobResult
|
||||||
|
from buildman.component.basecomponent import BaseComponent
|
||||||
|
from buildman.jobutil.buildpack import BuildPackage, BuildPackageException
|
||||||
|
from buildman.jobutil.buildstatus import StatusHandler
|
||||||
|
from buildman.jobutil.workererror import WorkerError
|
||||||
|
|
||||||
|
from data.database import BUILD_PHASE
|
||||||
|
|
||||||
|
HEARTBEAT_DELTA = datetime.timedelta(seconds=30)
|
||||||
|
HEARTBEAT_TIMEOUT = 10
|
||||||
|
INITIAL_TIMEOUT = 25
|
||||||
|
|
||||||
|
SUPPORTED_WORKER_VERSIONS = ['0.1-beta']
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ComponentStatus(object):
|
||||||
|
""" ComponentStatus represents the possible states of a component. """
|
||||||
|
JOINING = 'joining'
|
||||||
|
WAITING = 'waiting'
|
||||||
|
RUNNING = 'running'
|
||||||
|
BUILDING = 'building'
|
||||||
|
TIMED_OUT = 'timeout'
|
||||||
|
|
||||||
|
class BuildComponent(BaseComponent):
|
||||||
|
""" An application session component which conducts one (or more) builds. """
|
||||||
|
def __init__(self, config, realm=None, token=None, **kwargs):
|
||||||
|
self.expected_token = token
|
||||||
|
self.builder_realm = realm
|
||||||
|
|
||||||
|
self.parent_manager = None
|
||||||
|
self.server_hostname = None
|
||||||
|
|
||||||
|
self._component_status = ComponentStatus.JOINING
|
||||||
|
self._last_heartbeat = None
|
||||||
|
self._current_job = None
|
||||||
|
self._build_status = None
|
||||||
|
self._image_info = None
|
||||||
|
|
||||||
|
BaseComponent.__init__(self, config, **kwargs)
|
||||||
|
|
||||||
|
def onConnect(self):
|
||||||
|
self.join(self.builder_realm)
|
||||||
|
|
||||||
|
def onJoin(self, details):
|
||||||
|
logger.debug('Registering methods and listeners for component %s', self.builder_realm)
|
||||||
|
yield From(self.register(self._on_ready, u'io.quay.buildworker.ready'))
|
||||||
|
yield From(self.register(self._ping, u'io.quay.buildworker.ping'))
|
||||||
|
yield From(self.subscribe(self._on_heartbeat, 'io.quay.builder.heartbeat'))
|
||||||
|
yield From(self.subscribe(self._on_log_message, 'io.quay.builder.logmessage'))
|
||||||
|
|
||||||
|
self._set_status(ComponentStatus.WAITING)
|
||||||
|
|
||||||
|
def is_ready(self):
|
||||||
|
""" Determines whether a build component is ready to begin a build. """
|
||||||
|
return self._component_status == ComponentStatus.RUNNING
|
||||||
|
|
||||||
|
def start_build(self, build_job):
|
||||||
|
""" Starts a build. """
|
||||||
|
self._current_job = build_job
|
||||||
|
self._build_status = StatusHandler(self.build_logs, build_job.repo_build())
|
||||||
|
self._image_info = {}
|
||||||
|
|
||||||
|
self._set_status(ComponentStatus.BUILDING)
|
||||||
|
|
||||||
|
# Retrieve the job's buildpack.
|
||||||
|
buildpack_url = self.user_files.get_file_url(build_job.repo_build().resource_key,
|
||||||
|
requires_cors=False)
|
||||||
|
|
||||||
|
logger.debug('Retreiving build package: %s', buildpack_url)
|
||||||
|
buildpack = None
|
||||||
|
try:
|
||||||
|
buildpack = BuildPackage.from_url(buildpack_url)
|
||||||
|
except BuildPackageException as bpe:
|
||||||
|
self._build_failure('Could not retrieve build package', bpe)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Extract the base image information from the Dockerfile.
|
||||||
|
parsed_dockerfile = None
|
||||||
|
logger.debug('Parsing dockerfile')
|
||||||
|
|
||||||
|
build_config = build_job.build_config()
|
||||||
|
try:
|
||||||
|
parsed_dockerfile = buildpack.parse_dockerfile(build_config.get('build_subdir'))
|
||||||
|
except BuildPackageException as bpe:
|
||||||
|
self._build_failure('Could not find Dockerfile in build package', bpe)
|
||||||
|
return
|
||||||
|
|
||||||
|
image_and_tag_tuple = parsed_dockerfile.get_image_and_tag()
|
||||||
|
if image_and_tag_tuple is None or image_and_tag_tuple[0] is None:
|
||||||
|
self._build_failure('Missing FROM line in Dockerfile')
|
||||||
|
return
|
||||||
|
|
||||||
|
base_image_information = {
|
||||||
|
'repository': image_and_tag_tuple[0],
|
||||||
|
'tag': image_and_tag_tuple[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the number of steps from the Dockerfile.
|
||||||
|
with self._build_status as status_dict:
|
||||||
|
status_dict['total_commands'] = len(parsed_dockerfile.commands)
|
||||||
|
|
||||||
|
# Add the pull robot information, if any.
|
||||||
|
if build_config.get('pull_credentials') is not None:
|
||||||
|
base_image_information['username'] = build_config['pull_credentials'].get('username', '')
|
||||||
|
base_image_information['password'] = build_config['pull_credentials'].get('password', '')
|
||||||
|
|
||||||
|
# Retrieve the repository's fully qualified name.
|
||||||
|
repo = build_job.repo_build().repository
|
||||||
|
repository_name = repo.namespace_user.username + '/' + repo.name
|
||||||
|
|
||||||
|
# Parse the build queue item into build arguments.
|
||||||
|
# build_package: URL to the build package to download and untar/unzip.
|
||||||
|
# sub_directory: The location within the build package of the Dockerfile and the build context.
|
||||||
|
# repository: The repository for which this build is occurring.
|
||||||
|
# registry: The registry for which this build is occuring (e.g. 'quay.io', 'staging.quay.io').
|
||||||
|
# pull_token: The token to use when pulling the cache for building.
|
||||||
|
# push_token: The token to use to push the built image.
|
||||||
|
# tag_names: The name(s) of the tag(s) for the newly built image.
|
||||||
|
# base_image: The image name and credentials to use to conduct the base image pull.
|
||||||
|
# repository: The repository to pull.
|
||||||
|
# tag: The tag to pull.
|
||||||
|
# username: The username for pulling the base image (if any).
|
||||||
|
# password: The password for pulling the base image (if any).
|
||||||
|
build_arguments = {
|
||||||
|
'build_package': buildpack_url,
|
||||||
|
'sub_directory': build_config.get('build_subdir', ''),
|
||||||
|
'repository': repository_name,
|
||||||
|
'registry': self.server_hostname,
|
||||||
|
'pull_token': build_job.repo_build().access_token.code,
|
||||||
|
'push_token': build_job.repo_build().access_token.code,
|
||||||
|
'tag_names': build_config.get('docker_tags', ['latest']),
|
||||||
|
'base_image': base_image_information,
|
||||||
|
'cached_tag': build_job.determine_cached_tag() or ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# Invoke the build.
|
||||||
|
logger.debug('Invoking build: %s', self.builder_realm)
|
||||||
|
logger.debug('With Arguments: %s', build_arguments)
|
||||||
|
|
||||||
|
return (self
|
||||||
|
.call("io.quay.builder.build", **build_arguments)
|
||||||
|
.add_done_callback(self._build_complete))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _total_completion(statuses, total_images):
|
||||||
|
""" Returns the current amount completion relative to the total completion of a build. """
|
||||||
|
percentage_with_sizes = float(len(statuses.values())) / total_images
|
||||||
|
sent_bytes = sum([status['current'] for status in statuses.values()])
|
||||||
|
total_bytes = sum([status['total'] for status in statuses.values()])
|
||||||
|
return float(sent_bytes) / total_bytes * percentage_with_sizes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _process_pushpull_status(status_dict, current_phase, docker_data, images):
|
||||||
|
""" Processes the status of a push or pull by updating the provided status_dict and images. """
|
||||||
|
if not docker_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
num_images = 0
|
||||||
|
status_completion_key = ''
|
||||||
|
|
||||||
|
if current_phase == 'pushing':
|
||||||
|
status_completion_key = 'push_completion'
|
||||||
|
num_images = status_dict['total_commands']
|
||||||
|
elif current_phase == 'pulling':
|
||||||
|
status_completion_key = 'pull_completion'
|
||||||
|
elif current_phase == 'priming-cache':
|
||||||
|
status_completion_key = 'cache_completion'
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'progressDetail' in docker_data and 'id' in docker_data:
|
||||||
|
image_id = docker_data['id']
|
||||||
|
detail = docker_data['progressDetail']
|
||||||
|
|
||||||
|
if 'current' in detail and 'total' in detail:
|
||||||
|
images[image_id] = detail
|
||||||
|
status_dict[status_completion_key] = \
|
||||||
|
BuildComponent._total_completion(images, max(len(images), num_images))
|
||||||
|
|
||||||
|
def _on_log_message(self, phase, json_data):
|
||||||
|
""" Tails log messages and updates the build status. """
|
||||||
|
# Parse any of the JSON data logged.
|
||||||
|
docker_data = {}
|
||||||
|
if json_data:
|
||||||
|
try:
|
||||||
|
docker_data = json.loads(json_data)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extract the current status message (if any).
|
||||||
|
fully_unwrapped = ''
|
||||||
|
keys_to_extract = ['error', 'status', 'stream']
|
||||||
|
for key in keys_to_extract:
|
||||||
|
if key in docker_data:
|
||||||
|
fully_unwrapped = docker_data[key]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Determine if this is a step string.
|
||||||
|
current_step = None
|
||||||
|
current_status_string = str(fully_unwrapped.encode('utf-8'))
|
||||||
|
|
||||||
|
if current_status_string and phase == BUILD_PHASE.BUILDING:
|
||||||
|
step_increment = re.search(r'Step ([0-9]+) :', current_status_string)
|
||||||
|
if step_increment:
|
||||||
|
current_step = int(step_increment.group(1))
|
||||||
|
|
||||||
|
# Parse and update the phase and the status_dict. The status dictionary contains
|
||||||
|
# the pull/push progress, as well as the current step index.
|
||||||
|
with self._build_status as status_dict:
|
||||||
|
if self._build_status.set_phase(phase):
|
||||||
|
logger.debug('Build %s has entered a new phase: %s', self.builder_realm, phase)
|
||||||
|
|
||||||
|
BuildComponent._process_pushpull_status(status_dict, phase, docker_data, self._image_info)
|
||||||
|
|
||||||
|
# If the current message represents the beginning of a new step, then update the
|
||||||
|
# current command index.
|
||||||
|
if current_step is not None:
|
||||||
|
status_dict['current_command'] = current_step
|
||||||
|
|
||||||
|
# If the json data contains an error, then something went wrong with a push or pull.
|
||||||
|
if 'error' in docker_data:
|
||||||
|
self._build_status.set_error(docker_data['error'])
|
||||||
|
|
||||||
|
if current_step is not None:
|
||||||
|
self._build_status.set_command(current_status_string)
|
||||||
|
elif phase == BUILD_PHASE.BUILDING:
|
||||||
|
self._build_status.append_log(current_status_string)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_failure(self, error_message, exception=None):
|
||||||
|
""" Handles and logs a failed build. """
|
||||||
|
self._build_status.set_error(error_message, {
|
||||||
|
'internal_error': exception.message if exception else None
|
||||||
|
})
|
||||||
|
|
||||||
|
build_id = self._current_job.repo_build().uuid
|
||||||
|
logger.warning('Build %s failed with message: %s', build_id, error_message)
|
||||||
|
|
||||||
|
# Mark that the build has finished (in an error state)
|
||||||
|
self._build_finished(BuildJobResult.ERROR)
|
||||||
|
|
||||||
|
def _build_complete(self, result):
|
||||||
|
""" Wraps up a completed build. Handles any errors and calls self._build_finished. """
|
||||||
|
try:
|
||||||
|
# Retrieve the result. This will raise an ApplicationError on any error that occurred.
|
||||||
|
result.result()
|
||||||
|
self._build_status.set_phase(BUILD_PHASE.COMPLETE)
|
||||||
|
self._build_finished(BuildJobResult.COMPLETE)
|
||||||
|
except ApplicationError as aex:
|
||||||
|
worker_error = WorkerError(aex.error, aex.kwargs.get('base_error'))
|
||||||
|
|
||||||
|
# Write the error to the log.
|
||||||
|
self._build_status.set_error(worker_error.public_message(), worker_error.extra_data(),
|
||||||
|
internal_error=worker_error.is_internal_error())
|
||||||
|
|
||||||
|
# Mark the build as completed.
|
||||||
|
if worker_error.is_internal_error():
|
||||||
|
self._build_finished(BuildJobResult.INCOMPLETE)
|
||||||
|
else:
|
||||||
|
self._build_finished(BuildJobResult.ERROR)
|
||||||
|
|
||||||
|
def _build_finished(self, job_status):
|
||||||
|
""" Alerts the parent that a build has completed and sets the status back to running. """
|
||||||
|
self.parent_manager.job_completed(self._current_job, job_status, self)
|
||||||
|
self._current_job = None
|
||||||
|
|
||||||
|
# Set the component back to a running state.
|
||||||
|
self._set_status(ComponentStatus.RUNNING)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ping():
|
||||||
|
""" Ping pong. """
|
||||||
|
return 'pong'
|
||||||
|
|
||||||
|
def _on_ready(self, token, version):
|
||||||
|
if not version in SUPPORTED_WORKER_VERSIONS:
|
||||||
|
logger.warning('Build component (token "%s") is running an out-of-date version: %s', version)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._component_status != 'waiting':
|
||||||
|
logger.warning('Build component (token "%s") is already connected', self.expected_token)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if token != self.expected_token:
|
||||||
|
logger.warning('Builder token mismatch. Expected: "%s". Found: "%s"', self.expected_token, token)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._set_status(ComponentStatus.RUNNING)
|
||||||
|
|
||||||
|
# Start the heartbeat check and updating loop.
|
||||||
|
loop = trollius.get_event_loop()
|
||||||
|
loop.create_task(self._heartbeat())
|
||||||
|
logger.debug('Build worker %s is connected and ready', self.builder_realm)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _set_status(self, phase):
|
||||||
|
self._component_status = phase
|
||||||
|
|
||||||
|
def _on_heartbeat(self):
|
||||||
|
""" Updates the last known heartbeat. """
|
||||||
|
self._last_heartbeat = datetime.datetime.now()
|
||||||
|
|
||||||
|
@trollius.coroutine
|
||||||
|
def _heartbeat(self):
|
||||||
|
""" Coroutine that runs every HEARTBEAT_TIMEOUT seconds, both checking the worker's heartbeat
|
||||||
|
and updating the heartbeat in the build status dictionary (if applicable). This allows
|
||||||
|
the build system to catch crashes from either end.
|
||||||
|
"""
|
||||||
|
yield From(trollius.sleep(INITIAL_TIMEOUT))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# If the component is no longer running or actively building, nothing more to do.
|
||||||
|
if (self._component_status != ComponentStatus.RUNNING and
|
||||||
|
self._component_status != ComponentStatus.BUILDING):
|
||||||
|
return
|
||||||
|
|
||||||
|
# If there is an active build, write the heartbeat to its status.
|
||||||
|
build_status = self._build_status
|
||||||
|
if build_status is not None:
|
||||||
|
with build_status as status_dict:
|
||||||
|
status_dict['heartbeat'] = int(time.time())
|
||||||
|
|
||||||
|
|
||||||
|
# Mark the build item.
|
||||||
|
current_job = self._current_job
|
||||||
|
if current_job is not None:
|
||||||
|
self.parent_manager.job_heartbeat(current_job)
|
||||||
|
|
||||||
|
# Check the heartbeat from the worker.
|
||||||
|
logger.debug('Checking heartbeat on realm %s', self.builder_realm)
|
||||||
|
if self._last_heartbeat and self._last_heartbeat < datetime.datetime.now() - HEARTBEAT_DELTA:
|
||||||
|
self._timeout()
|
||||||
|
return
|
||||||
|
|
||||||
|
yield From(trollius.sleep(HEARTBEAT_TIMEOUT))
|
||||||
|
|
||||||
|
def _timeout(self):
|
||||||
|
self._set_status(ComponentStatus.TIMED_OUT)
|
||||||
|
logger.warning('Build component with realm %s has timed out', self.builder_realm)
|
||||||
|
self._dispose(timed_out=True)
|
||||||
|
|
||||||
|
def _dispose(self, timed_out=False):
|
||||||
|
# If we still have a running job, then it has not completed and we need to tell the parent
|
||||||
|
# manager.
|
||||||
|
if self._current_job is not None:
|
||||||
|
if timed_out:
|
||||||
|
self._build_status.set_error('Build worker timed out', internal_error=True)
|
||||||
|
|
||||||
|
self.parent_manager.job_completed(self._current_job, BuildJobResult.INCOMPLETE, self)
|
||||||
|
self._build_status = None
|
||||||
|
self._current_job = None
|
||||||
|
|
||||||
|
# Unregister the current component so that it cannot be invoked again.
|
||||||
|
self.parent_manager.build_component_disposed(self, timed_out)
|
0
buildman/jobutil/__init__.py
Normal file
0
buildman/jobutil/__init__.py
Normal file
60
buildman/jobutil/buildjob.py
Normal file
60
buildman/jobutil/buildjob.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from data import model
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
class BuildJobLoadException(Exception):
|
||||||
|
""" Exception raised if a build job could not be instantiated for some reason. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BuildJob(object):
|
||||||
|
""" Represents a single in-progress build job. """
|
||||||
|
def __init__(self, job_item):
|
||||||
|
self._job_item = job_item
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._job_details = json.loads(job_item.body)
|
||||||
|
except ValueError:
|
||||||
|
raise BuildJobLoadException(
|
||||||
|
'Could not parse build queue item config with ID %s' % self._job_details['build_uuid']
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._repo_build = model.get_repository_build(self._job_details['namespace'],
|
||||||
|
self._job_details['repository'],
|
||||||
|
self._job_details['build_uuid'])
|
||||||
|
except model.InvalidRepositoryBuildException:
|
||||||
|
raise BuildJobLoadException(
|
||||||
|
'Could not load repository build with ID %s' % self._job_details['build_uuid'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._build_config = json.loads(self._repo_build.job_config)
|
||||||
|
except ValueError:
|
||||||
|
raise BuildJobLoadException(
|
||||||
|
'Could not parse repository build job config with ID %s' % self._job_details['build_uuid']
|
||||||
|
)
|
||||||
|
|
||||||
|
def determine_cached_tag(self):
|
||||||
|
""" Returns the tag to pull to prime the cache or None if none. """
|
||||||
|
# TODO(jschorr): Change this to use the more complicated caching rules, once we have caching
|
||||||
|
# be a pull of things besides the constructed tags.
|
||||||
|
tags = self._build_config.get('docker_tags', ['latest'])
|
||||||
|
existing_tags = model.list_repository_tags(self._job_details['namespace'],
|
||||||
|
self._job_details['repository'])
|
||||||
|
|
||||||
|
cached_tags = set(tags) & set([tag.name for tag in existing_tags])
|
||||||
|
if cached_tags:
|
||||||
|
return list(cached_tags)[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def job_item(self):
|
||||||
|
""" Returns the job's queue item. """
|
||||||
|
return self._job_item
|
||||||
|
|
||||||
|
def repo_build(self):
|
||||||
|
""" Returns the repository build DB row for the job. """
|
||||||
|
return self._repo_build
|
||||||
|
|
||||||
|
def build_config(self):
|
||||||
|
""" Returns the parsed repository build config for the job. """
|
||||||
|
return self._build_config
|
88
buildman/jobutil/buildpack.py
Normal file
88
buildman/jobutil/buildpack.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import tarfile
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
from tempfile import TemporaryFile, mkdtemp
|
||||||
|
from zipfile import ZipFile
|
||||||
|
from util.dockerfileparse import parse_dockerfile
|
||||||
|
from util.safetar import safe_extractall
|
||||||
|
|
||||||
|
class BuildPackageException(Exception):
|
||||||
|
""" Exception raised when retrieving or parsing a build package. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BuildPackage(object):
|
||||||
|
""" Helper class for easy reading and updating of a Dockerfile build pack. """
|
||||||
|
|
||||||
|
def __init__(self, requests_file):
|
||||||
|
self._mime_processors = {
|
||||||
|
'application/zip': BuildPackage._prepare_zip,
|
||||||
|
'application/x-zip-compressed': BuildPackage._prepare_zip,
|
||||||
|
'text/plain': BuildPackage._prepare_dockerfile,
|
||||||
|
'application/octet-stream': BuildPackage._prepare_dockerfile,
|
||||||
|
'application/x-tar': BuildPackage._prepare_tarball,
|
||||||
|
'application/gzip': BuildPackage._prepare_tarball,
|
||||||
|
'application/x-gzip': BuildPackage._prepare_tarball,
|
||||||
|
}
|
||||||
|
|
||||||
|
c_type = requests_file.headers['content-type']
|
||||||
|
c_type = c_type.split(';')[0] if ';' in c_type else c_type
|
||||||
|
|
||||||
|
if c_type not in self._mime_processors:
|
||||||
|
raise BuildPackageException('Unknown build package mime type: %s' % c_type)
|
||||||
|
|
||||||
|
self._package_directory = None
|
||||||
|
try:
|
||||||
|
self._package_directory = self._mime_processors[c_type](requests_file)
|
||||||
|
except Exception as ex:
|
||||||
|
raise BuildPackageException(ex.message)
|
||||||
|
|
||||||
|
def parse_dockerfile(self, subdirectory):
|
||||||
|
dockerfile_path = os.path.join(self._package_directory, subdirectory, 'Dockerfile')
|
||||||
|
if not os.path.exists(dockerfile_path):
|
||||||
|
if subdirectory:
|
||||||
|
message = 'Build package did not contain a Dockerfile at sub directory %s.' % subdirectory
|
||||||
|
else:
|
||||||
|
message = 'Build package did not contain a Dockerfile at the root directory.'
|
||||||
|
|
||||||
|
raise BuildPackageException(message)
|
||||||
|
|
||||||
|
with open(dockerfile_path, 'r') as dockerfileobj:
|
||||||
|
return parse_dockerfile(dockerfileobj.read())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_url(url):
|
||||||
|
buildpack_resource = requests.get(url, stream=True)
|
||||||
|
return BuildPackage(buildpack_resource)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prepare_zip(request_file):
|
||||||
|
build_dir = mkdtemp(prefix='docker-build-')
|
||||||
|
|
||||||
|
# Save the zip file to temp somewhere
|
||||||
|
with TemporaryFile() as zip_file:
|
||||||
|
zip_file.write(request_file.content)
|
||||||
|
to_extract = ZipFile(zip_file)
|
||||||
|
to_extract.extractall(build_dir)
|
||||||
|
|
||||||
|
return build_dir
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prepare_dockerfile(request_file):
|
||||||
|
build_dir = mkdtemp(prefix='docker-build-')
|
||||||
|
dockerfile_path = os.path.join(build_dir, "Dockerfile")
|
||||||
|
with open(dockerfile_path, 'w') as dockerfile:
|
||||||
|
dockerfile.write(request_file.content)
|
||||||
|
|
||||||
|
return build_dir
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prepare_tarball(request_file):
|
||||||
|
build_dir = mkdtemp(prefix='docker-build-')
|
||||||
|
|
||||||
|
# Save the zip file to temp somewhere
|
||||||
|
with tarfile.open(mode='r|*', fileobj=request_file.raw) as tar_stream:
|
||||||
|
safe_extractall(tar_stream, build_dir)
|
||||||
|
|
||||||
|
return build_dir
|
52
buildman/jobutil/buildstatus.py
Normal file
52
buildman/jobutil/buildstatus.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from data.database import BUILD_PHASE
|
||||||
|
|
||||||
|
class StatusHandler(object):
|
||||||
|
""" Context wrapper for writing status to build logs. """
|
||||||
|
|
||||||
|
def __init__(self, build_logs, repository_build):
|
||||||
|
self._current_phase = None
|
||||||
|
self._repository_build = repository_build
|
||||||
|
self._uuid = repository_build.uuid
|
||||||
|
self._build_logs = build_logs
|
||||||
|
|
||||||
|
self._status = {
|
||||||
|
'total_commands': None,
|
||||||
|
'current_command': None,
|
||||||
|
'push_completion': 0.0,
|
||||||
|
'pull_completion': 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write the initial status.
|
||||||
|
self.__exit__(None, None, None)
|
||||||
|
|
||||||
|
def _append_log_message(self, log_message, log_type=None, log_data=None):
|
||||||
|
self._build_logs.append_log_message(self._uuid, log_message, log_type, log_data)
|
||||||
|
|
||||||
|
def append_log(self, log_message, extra_data=None):
|
||||||
|
self._append_log_message(log_message, log_data=extra_data)
|
||||||
|
|
||||||
|
def set_command(self, command, extra_data=None):
|
||||||
|
self._append_log_message(command, self._build_logs.COMMAND, extra_data)
|
||||||
|
|
||||||
|
def set_error(self, error_message, extra_data=None, internal_error=False):
|
||||||
|
self.set_phase(BUILD_PHASE.INTERNAL_ERROR if internal_error else BUILD_PHASE.ERROR)
|
||||||
|
|
||||||
|
extra_data = extra_data or {}
|
||||||
|
extra_data['internal_error'] = internal_error
|
||||||
|
self._append_log_message(error_message, self._build_logs.ERROR, extra_data)
|
||||||
|
|
||||||
|
def set_phase(self, phase, extra_data=None):
|
||||||
|
if phase == self._current_phase:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._current_phase = phase
|
||||||
|
self._append_log_message(phase, self._build_logs.PHASE, extra_data)
|
||||||
|
self._repository_build.phase = phase
|
||||||
|
self._repository_build.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self._status
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, value, traceback):
|
||||||
|
self._build_logs.set_status(self._uuid, self._status)
|
84
buildman/jobutil/workererror.py
Normal file
84
buildman/jobutil/workererror.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
class WorkerError(object):
|
||||||
|
""" Helper class which represents errors raised by a build worker. """
|
||||||
|
def __init__(self, error_code, base_message=None):
|
||||||
|
self._error_code = error_code
|
||||||
|
self._base_message = base_message
|
||||||
|
|
||||||
|
self._error_handlers = {
|
||||||
|
'io.quay.builder.buildpackissue': {
|
||||||
|
'message': 'Could not load build package',
|
||||||
|
'is_internal': True
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.cannotextractbuildpack': {
|
||||||
|
'message': 'Could not extract the contents of the build package'
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.cannotpullforcache': {
|
||||||
|
'message': 'Could not pull cached image',
|
||||||
|
'is_internal': True
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.cannotpullbaseimage': {
|
||||||
|
'message': 'Could not pull base image',
|
||||||
|
'show_base_error': True
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.internalerror': {
|
||||||
|
'message': 'An internal error occurred while building. Please submit a ticket.'
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.buildrunerror': {
|
||||||
|
'message': 'Could not start the build process',
|
||||||
|
'is_internal': True
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.builderror': {
|
||||||
|
'message': 'A build step failed',
|
||||||
|
'show_base_error': True
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.tagissue': {
|
||||||
|
'message': 'Could not tag built image',
|
||||||
|
'is_internal': True
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.pushissue': {
|
||||||
|
'message': 'Could not push built image',
|
||||||
|
'show_base_error': True,
|
||||||
|
'is_internal': True
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.dockerconnecterror': {
|
||||||
|
'message': 'Could not connect to Docker daemon',
|
||||||
|
'is_internal': True
|
||||||
|
},
|
||||||
|
|
||||||
|
'io.quay.builder.missingorinvalidargument': {
|
||||||
|
'message': 'Missing required arguments for builder',
|
||||||
|
'is_internal': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_internal_error(self):
|
||||||
|
handler = self._error_handlers.get(self._error_code)
|
||||||
|
return handler.get('is_internal', False) if handler else True
|
||||||
|
|
||||||
|
def public_message(self):
|
||||||
|
handler = self._error_handlers.get(self._error_code)
|
||||||
|
if not handler:
|
||||||
|
return 'An unknown error occurred'
|
||||||
|
|
||||||
|
message = handler['message']
|
||||||
|
if handler.get('show_base_error', False) and self._base_message:
|
||||||
|
message = message + ': ' + self._base_message
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
def extra_data(self):
|
||||||
|
if self._base_message:
|
||||||
|
return {
|
||||||
|
'base_error': self._base_message
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
0
buildman/manager/__init__.py
Normal file
0
buildman/manager/__init__.py
Normal file
49
buildman/manager/basemanager.py
Normal file
49
buildman/manager/basemanager.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
class BaseManager(object):
|
||||||
|
""" Base for all worker managers. """
|
||||||
|
def __init__(self, register_component, unregister_component, job_heartbeat_callback,
|
||||||
|
job_complete_callback):
|
||||||
|
self.register_component = register_component
|
||||||
|
self.unregister_component = unregister_component
|
||||||
|
self.job_heartbeat_callback = job_heartbeat_callback
|
||||||
|
self.job_complete_callback = job_complete_callback
|
||||||
|
|
||||||
|
def job_heartbeat(self, build_job):
|
||||||
|
""" Method invoked to tell the manager that a job is still running. This method will be called
|
||||||
|
every few minutes. """
|
||||||
|
self.job_heartbeat_callback(build_job)
|
||||||
|
|
||||||
|
def setup_time(self):
|
||||||
|
""" Returns the number of seconds that the build system should wait before allowing the job
|
||||||
|
to be picked up again after called 'schedule'.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
""" Indicates that the build controller server is in a shutdown state and that no new jobs
|
||||||
|
or workers should be performed. Existing workers should be cleaned up once their jobs
|
||||||
|
have completed
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def schedule(self, build_job, loop):
|
||||||
|
""" Schedules a queue item to be built. Returns True if the item was properly scheduled
|
||||||
|
and False if all workers are busy.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
""" Runs any initialization code for the manager. Called once the server is in a ready state.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def build_component_disposed(self, build_component, timed_out):
|
||||||
|
""" Method invoked whenever a build component has been disposed. The timed_out boolean indicates
|
||||||
|
whether the component's heartbeat timed out.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def job_completed(self, build_job, job_status, build_component):
|
||||||
|
""" Method invoked once a job_item has completed, in some manner. The job_status will be
|
||||||
|
one of: incomplete, error, complete. If incomplete, the job should be requeued.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
72
buildman/manager/enterprise.py
Normal file
72
buildman/manager/enterprise.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from buildman.component.basecomponent import BaseComponent
|
||||||
|
from buildman.component.buildcomponent import BuildComponent
|
||||||
|
from buildman.manager.basemanager import BaseManager
|
||||||
|
|
||||||
|
from trollius.coroutines import From
|
||||||
|
|
||||||
|
REGISTRATION_REALM = 'registration'
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DynamicRegistrationComponent(BaseComponent):
|
||||||
|
""" Component session that handles dynamic registration of the builder components. """
|
||||||
|
|
||||||
|
def onConnect(self):
|
||||||
|
self.join(REGISTRATION_REALM)
|
||||||
|
|
||||||
|
def onJoin(self, details):
|
||||||
|
logger.debug('Registering registration method')
|
||||||
|
yield From(self.register(self._worker_register, u'io.quay.buildworker.register'))
|
||||||
|
|
||||||
|
def _worker_register(self):
|
||||||
|
realm = self.parent_manager.add_build_component()
|
||||||
|
logger.debug('Registering new build component+worker with realm %s', realm)
|
||||||
|
return realm
|
||||||
|
|
||||||
|
|
||||||
|
class EnterpriseManager(BaseManager):
|
||||||
|
""" Build manager implementation for the Enterprise Registry. """
|
||||||
|
build_components = []
|
||||||
|
shutting_down = False
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
# Add a component which is used by build workers for dynamic registration. Unlike
|
||||||
|
# production, build workers in enterprise are long-lived and register dynamically.
|
||||||
|
self.register_component(REGISTRATION_REALM, DynamicRegistrationComponent)
|
||||||
|
|
||||||
|
def setup_time(self):
|
||||||
|
# Builders are already registered, so the setup time should be essentially instant. We therefore
|
||||||
|
# only return a minute here.
|
||||||
|
return 60
|
||||||
|
|
||||||
|
def add_build_component(self):
|
||||||
|
""" Adds a new build component for an Enterprise Registry. """
|
||||||
|
# Generate a new unique realm ID for the build worker.
|
||||||
|
realm = str(uuid.uuid4())
|
||||||
|
component = self.register_component(realm, BuildComponent, token="")
|
||||||
|
self.build_components.append(component)
|
||||||
|
return realm
|
||||||
|
|
||||||
|
def schedule(self, build_job, loop):
|
||||||
|
""" Schedules a build for an Enterprise Registry. """
|
||||||
|
if self.shutting_down:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for component in self.build_components:
|
||||||
|
if component.is_ready():
|
||||||
|
loop.call_soon(component.start_build, build_job)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.shutting_down = True
|
||||||
|
|
||||||
|
def job_completed(self, build_job, job_status, build_component):
|
||||||
|
self.job_complete_callback(build_job, job_status)
|
||||||
|
|
||||||
|
def build_component_disposed(self, build_component, timed_out):
|
||||||
|
self.build_components.remove(build_component)
|
||||||
|
|
177
buildman/server.py
Normal file
177
buildman/server.py
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
import logging
|
||||||
|
import trollius
|
||||||
|
|
||||||
|
from autobahn.asyncio.wamp import RouterFactory, RouterSessionFactory
|
||||||
|
from autobahn.asyncio.websocket import WampWebSocketServerFactory
|
||||||
|
from autobahn.wamp import types
|
||||||
|
|
||||||
|
from aiowsgi import create_server as create_wsgi_server
|
||||||
|
from flask import Flask
|
||||||
|
from threading import Event
|
||||||
|
from trollius.coroutines import From
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from buildman.jobutil.buildjob import BuildJob, BuildJobLoadException
|
||||||
|
from data.queue import WorkQueue
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
WORK_CHECK_TIMEOUT = 10
|
||||||
|
TIMEOUT_PERIOD_MINUTES = 20
|
||||||
|
JOB_TIMEOUT_SECONDS = 300
|
||||||
|
MINIMUM_JOB_EXTENSION = timedelta(minutes=2)
|
||||||
|
|
||||||
|
WEBSOCKET_PORT = 8787
|
||||||
|
CONTROLLER_PORT = 8686
|
||||||
|
|
||||||
|
class BuildJobResult(object):
|
||||||
|
""" Build job result enum """
|
||||||
|
INCOMPLETE = 'incomplete'
|
||||||
|
COMPLETE = 'complete'
|
||||||
|
ERROR = 'error'
|
||||||
|
|
||||||
|
class BuilderServer(object):
|
||||||
|
""" Server which handles both HTTP and WAMP requests, managing the full state of the build
|
||||||
|
controller.
|
||||||
|
"""
|
||||||
|
def __init__(self, server_hostname, queue, build_logs, user_files, lifecycle_manager_klass):
|
||||||
|
self._loop = None
|
||||||
|
self._current_status = 'starting'
|
||||||
|
self._current_components = []
|
||||||
|
self._job_count = 0
|
||||||
|
|
||||||
|
self._session_factory = RouterSessionFactory(RouterFactory())
|
||||||
|
self._server_hostname = server_hostname
|
||||||
|
self._queue = queue
|
||||||
|
self._build_logs = build_logs
|
||||||
|
self._user_files = user_files
|
||||||
|
self._lifecycle_manager = lifecycle_manager_klass(
|
||||||
|
self._register_component,
|
||||||
|
self._unregister_component,
|
||||||
|
self._job_heartbeat,
|
||||||
|
self._job_complete
|
||||||
|
)
|
||||||
|
|
||||||
|
self._shutdown_event = Event()
|
||||||
|
self._current_status = 'running'
|
||||||
|
|
||||||
|
self._register_controller()
|
||||||
|
|
||||||
|
def _register_controller(self):
|
||||||
|
controller_app = Flask('controller')
|
||||||
|
server = self
|
||||||
|
|
||||||
|
@controller_app.route('/status')
|
||||||
|
def status():
|
||||||
|
return server._current_status
|
||||||
|
|
||||||
|
self._controller_app = controller_app
|
||||||
|
|
||||||
|
def run(self, host, ssl=None):
|
||||||
|
logger.debug('Initializing the lifecycle manager')
|
||||||
|
self._lifecycle_manager.initialize()
|
||||||
|
|
||||||
|
logger.debug('Initializing all members of the event loop')
|
||||||
|
loop = trollius.get_event_loop()
|
||||||
|
trollius.Task(self._initialize(loop, host, ssl))
|
||||||
|
|
||||||
|
logger.debug('Starting server on port %s, with controller on port %s', WEBSOCKET_PORT,
|
||||||
|
CONTROLLER_PORT)
|
||||||
|
try:
|
||||||
|
loop.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
logger.debug('Requested server shutdown')
|
||||||
|
self._current_status = 'shutting_down'
|
||||||
|
self._lifecycle_manager.shutdown()
|
||||||
|
self._shutdown_event.wait()
|
||||||
|
logger.debug('Shutting down server')
|
||||||
|
|
||||||
|
def _register_component(self, realm, component_klass, **kwargs):
|
||||||
|
""" Registers a component with the server. The component_klass must derive from
|
||||||
|
BaseComponent.
|
||||||
|
"""
|
||||||
|
logger.debug('Registering component with realm %s', realm)
|
||||||
|
|
||||||
|
component = component_klass(types.ComponentConfig(realm=realm), realm=realm, **kwargs)
|
||||||
|
component.server = self
|
||||||
|
component.parent_manager = self._lifecycle_manager
|
||||||
|
component.build_logs = self._build_logs
|
||||||
|
component.user_files = self._user_files
|
||||||
|
component.server_hostname = self._server_hostname
|
||||||
|
|
||||||
|
self._current_components.append(component)
|
||||||
|
self._session_factory.add(component)
|
||||||
|
return component
|
||||||
|
|
||||||
|
def _unregister_component(self, component):
|
||||||
|
logger.debug('Unregistering component with realm %s and token %s',
|
||||||
|
component.builder_realm, component.expected_token)
|
||||||
|
|
||||||
|
self._current_components.remove(component)
|
||||||
|
self._session_factory.remove(component)
|
||||||
|
|
||||||
|
def _job_heartbeat(self, build_job):
|
||||||
|
WorkQueue.extend_processing(build_job.job_item(), seconds_from_now=JOB_TIMEOUT_SECONDS,
|
||||||
|
retry_count=1, minimum_extension=MINIMUM_JOB_EXTENSION)
|
||||||
|
|
||||||
|
def _job_complete(self, build_job, job_status):
|
||||||
|
if job_status == BuildJobResult.INCOMPLETE:
|
||||||
|
self._queue.incomplete(build_job.job_item(), restore_retry=True, retry_after=30)
|
||||||
|
elif job_status == BuildJobResult.ERROR:
|
||||||
|
self._queue.incomplete(build_job.job_item(), restore_retry=False)
|
||||||
|
else:
|
||||||
|
self._queue.complete(build_job.job_item())
|
||||||
|
|
||||||
|
self._job_count = self._job_count - 1
|
||||||
|
|
||||||
|
if self._current_status == 'shutting_down' and not self._job_count:
|
||||||
|
self._shutdown_event.set()
|
||||||
|
|
||||||
|
# TODO(jschorr): check for work here?
|
||||||
|
|
||||||
|
@trollius.coroutine
|
||||||
|
def _work_checker(self):
|
||||||
|
while self._current_status == 'running':
|
||||||
|
logger.debug('Checking for more work')
|
||||||
|
job_item = self._queue.get(processing_time=self._lifecycle_manager.setup_time())
|
||||||
|
if job_item is None:
|
||||||
|
logger.debug('No additional work found. Going to sleep for %s seconds', WORK_CHECK_TIMEOUT)
|
||||||
|
yield From(trollius.sleep(WORK_CHECK_TIMEOUT))
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
build_job = BuildJob(job_item)
|
||||||
|
except BuildJobLoadException as irbe:
|
||||||
|
logger.exception(irbe)
|
||||||
|
self._queue.incomplete(job_item, restore_retry=False)
|
||||||
|
|
||||||
|
logger.debug('Build job found. Checking for an avaliable worker.')
|
||||||
|
if self._lifecycle_manager.schedule(build_job, self._loop):
|
||||||
|
self._job_count = self._job_count + 1
|
||||||
|
logger.debug('Build job scheduled. Running: %s', self._job_count)
|
||||||
|
else:
|
||||||
|
logger.debug('All workers are busy. Requeuing.')
|
||||||
|
self._queue.incomplete(job_item, restore_retry=True, retry_after=0)
|
||||||
|
|
||||||
|
yield From(trollius.sleep(WORK_CHECK_TIMEOUT))
|
||||||
|
|
||||||
|
|
||||||
|
@trollius.coroutine
|
||||||
|
def _initialize(self, loop, host, ssl=None):
|
||||||
|
self._loop = loop
|
||||||
|
|
||||||
|
# Create the WAMP server.
|
||||||
|
transport_factory = WampWebSocketServerFactory(self._session_factory, debug_wamp=False)
|
||||||
|
transport_factory.setProtocolOptions(failByDrop=True)
|
||||||
|
|
||||||
|
# Initialize the controller server and the WAMP server
|
||||||
|
create_wsgi_server(self._controller_app, loop=loop, host=host, port=CONTROLLER_PORT, ssl=ssl)
|
||||||
|
yield From(loop.create_server(transport_factory, host, WEBSOCKET_PORT, ssl=ssl))
|
||||||
|
|
||||||
|
# Initialize the work queue checker.
|
||||||
|
yield From(self._work_checker())
|
|
@ -23,3 +23,11 @@ upstream verbs_app_server {
|
||||||
upstream registry_app_server {
|
upstream registry_app_server {
|
||||||
server unix:/tmp/gunicorn_registry.sock fail_timeout=0;
|
server unix:/tmp/gunicorn_registry.sock fail_timeout=0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstream build_manager_controller_server {
|
||||||
|
server localhost:8686;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream build_manager_websocket_server {
|
||||||
|
server localhost:8787;
|
||||||
|
}
|
2
conf/init/buildmanager/log/run
Executable file
2
conf/init/buildmanager/log/run
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec svlogd /var/log/buildmanager/
|
8
conf/init/buildmanager/run
Executable file
8
conf/init/buildmanager/run
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
echo 'Starting internal build manager'
|
||||||
|
|
||||||
|
cd /
|
||||||
|
venv/bin/python -m buildman.builder 2>&1
|
||||||
|
|
||||||
|
echo 'Internal build manager exited'
|
|
@ -60,3 +60,15 @@ location /v1/_ping {
|
||||||
add_header X-Docker-Registry-Standalone 0;
|
add_header X-Docker-Registry-Standalone 0;
|
||||||
return 200 'true';
|
return 200 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/b1/controller(/?)(.*) {
|
||||||
|
proxy_pass http://build_manager_controller_server/$2;
|
||||||
|
proxy_read_timeout 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/b1/socket(/?)(.*) {
|
||||||
|
proxy_pass http://build_manager_websocket_server/$2;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ def build_requests_session():
|
||||||
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
|
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
|
||||||
'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN',
|
'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN',
|
||||||
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
|
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
|
||||||
'CONTACT_INFO']
|
'CONTACT_INFO', 'AVATAR_KIND']
|
||||||
|
|
||||||
|
|
||||||
def getFrontendVisibleConfig(config_dict):
|
def getFrontendVisibleConfig(config_dict):
|
||||||
|
@ -46,6 +46,8 @@ class DefaultConfig(object):
|
||||||
PREFERRED_URL_SCHEME = 'http'
|
PREFERRED_URL_SCHEME = 'http'
|
||||||
SERVER_HOSTNAME = 'localhost:5000'
|
SERVER_HOSTNAME = 'localhost:5000'
|
||||||
|
|
||||||
|
AVATAR_KIND = 'local'
|
||||||
|
|
||||||
REGISTRY_TITLE = 'Quay.io'
|
REGISTRY_TITLE = 'Quay.io'
|
||||||
REGISTRY_TITLE_SHORT = 'Quay.io'
|
REGISTRY_TITLE_SHORT = 'Quay.io'
|
||||||
CONTACT_INFO = [
|
CONTACT_INFO = [
|
||||||
|
@ -163,6 +165,8 @@ class DefaultConfig(object):
|
||||||
# Feature Flag: Whether users can be renamed
|
# Feature Flag: Whether users can be renamed
|
||||||
FEATURE_USER_RENAME = False
|
FEATURE_USER_RENAME = False
|
||||||
|
|
||||||
|
BUILD_MANAGER = ('enterprise', {})
|
||||||
|
|
||||||
DISTRIBUTED_STORAGE_CONFIG = {
|
DISTRIBUTED_STORAGE_CONFIG = {
|
||||||
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}],
|
||||||
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}],
|
||||||
|
|
|
@ -420,6 +420,7 @@ class RepositoryTag(BaseModel):
|
||||||
class BUILD_PHASE(object):
|
class BUILD_PHASE(object):
|
||||||
""" Build phases enum """
|
""" Build phases enum """
|
||||||
ERROR = 'error'
|
ERROR = 'error'
|
||||||
|
INTERNAL_ERROR = 'internalerror'
|
||||||
UNPACKING = 'unpacking'
|
UNPACKING = 'unpacking'
|
||||||
PULLING = 'pulling'
|
PULLING = 'pulling'
|
||||||
BUILDING = 'building'
|
BUILDING = 'building'
|
||||||
|
@ -474,7 +475,7 @@ class OAuthApplication(BaseModel):
|
||||||
|
|
||||||
name = CharField()
|
name = CharField()
|
||||||
description = TextField(default='')
|
description = TextField(default='')
|
||||||
gravatar_email = CharField(null=True)
|
avatar_email = CharField(null=True, db_column='gravatar_email')
|
||||||
|
|
||||||
|
|
||||||
class OAuthAuthorizationCode(BaseModel):
|
class OAuthAuthorizationCode(BaseModel):
|
||||||
|
|
|
@ -9,6 +9,7 @@ from data.database import (OAuthApplication, OAuthAuthorizationCode, OAuthAccess
|
||||||
random_string_generator)
|
random_string_generator)
|
||||||
from data.model.legacy import get_user
|
from data.model.legacy import get_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
from flask import render_template
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -154,6 +155,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
|
|
||||||
|
|
||||||
def get_token_response(self, response_type, client_id, redirect_uri, **params):
|
def get_token_response(self, response_type, client_id, redirect_uri, **params):
|
||||||
|
|
||||||
# Ensure proper response_type
|
# Ensure proper response_type
|
||||||
if response_type != 'token':
|
if response_type != 'token':
|
||||||
err = 'unsupported_response_type'
|
err = 'unsupported_response_type'
|
||||||
|
@ -161,7 +163,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
|
|
||||||
# Check redirect URI
|
# Check redirect URI
|
||||||
is_valid_redirect_uri = self.validate_redirect_uri(client_id, redirect_uri)
|
is_valid_redirect_uri = self.validate_redirect_uri(client_id, redirect_uri)
|
||||||
if not is_valid_redirect_uri:
|
if redirect_uri != 'display' and not is_valid_redirect_uri:
|
||||||
return self._invalid_redirect_uri_response()
|
return self._invalid_redirect_uri_response()
|
||||||
|
|
||||||
# Check conditions
|
# Check conditions
|
||||||
|
@ -196,6 +198,10 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||||
url = utils.build_url(redirect_uri, params)
|
url = utils.build_url(redirect_uri, params)
|
||||||
url += '#access_token=%s&token_type=%s&expires_in=%s' % (access_token, token_type, expires_in)
|
url += '#access_token=%s&token_type=%s&expires_in=%s' % (access_token, token_type, expires_in)
|
||||||
|
|
||||||
|
if redirect_uri == 'display':
|
||||||
|
return self._make_response(
|
||||||
|
render_template("message.html", message="Access Token: " + access_token))
|
||||||
|
|
||||||
return self._make_response(headers={'Location': url}, status_code=302)
|
return self._make_response(headers={'Location': url}, status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -127,12 +127,16 @@ class WorkQueue(object):
|
||||||
incomplete_item_obj.save()
|
incomplete_item_obj.save()
|
||||||
self._currently_processing = False
|
self._currently_processing = False
|
||||||
|
|
||||||
def extend_processing(self, queue_item, seconds_from_now):
|
@staticmethod
|
||||||
|
def extend_processing(queue_item_info, seconds_from_now, retry_count=None,
|
||||||
|
minimum_extension=MINIMUM_EXTENSION):
|
||||||
|
queue_item = QueueItem.get(QueueItem.id == queue_item_info.id)
|
||||||
new_expiration = datetime.utcnow() + timedelta(seconds=seconds_from_now)
|
new_expiration = datetime.utcnow() + timedelta(seconds=seconds_from_now)
|
||||||
|
|
||||||
# Only actually write the new expiration to the db if it moves the expiration some minimum
|
# Only actually write the new expiration to the db if it moves the expiration some minimum
|
||||||
queue_item_obj = QueueItem.get(QueueItem.id == queue_item.id)
|
if new_expiration - queue_item.processing_expires > minimum_extension:
|
||||||
if new_expiration - queue_item_obj.processing_expires > MINIMUM_EXTENSION:
|
if retry_count is not None:
|
||||||
with self._transaction_factory(db):
|
queue_item.retries_remaining = retry_count
|
||||||
queue_item_obj.processing_expires = new_expiration
|
|
||||||
queue_item_obj.save()
|
queue_item.processing_expires = new_expiration
|
||||||
|
queue_item.save()
|
|
@ -1,11 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from app import app
|
||||||
from flask import Blueprint, request, make_response, jsonify, session
|
from flask import Blueprint, request, make_response, jsonify, session
|
||||||
from flask.ext.restful import Resource, abort, Api, reqparse
|
from flask.ext.restful import Resource, abort, Api, reqparse
|
||||||
from flask.ext.restful.utils.cors import crossdomain
|
from flask.ext.restful.utils.cors import crossdomain
|
||||||
from werkzeug.exceptions import HTTPException
|
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
|
@ -52,6 +51,10 @@ class InvalidRequest(ApiException):
|
||||||
def __init__(self, error_description, payload=None):
|
def __init__(self, error_description, payload=None):
|
||||||
ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
|
ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
|
||||||
|
|
||||||
|
class InvalidResponse(ApiException):
|
||||||
|
def __init__(self, error_description, payload=None):
|
||||||
|
ApiException.__init__(self, 'invalid_response', 400, error_description, payload)
|
||||||
|
|
||||||
|
|
||||||
class InvalidToken(ApiException):
|
class InvalidToken(ApiException):
|
||||||
def __init__(self, error_description, payload=None):
|
def __init__(self, error_description, payload=None):
|
||||||
|
@ -170,6 +173,9 @@ internal_only = add_method_metadata('internal', True)
|
||||||
|
|
||||||
def path_param(name, description):
|
def path_param(name, description):
|
||||||
def add_param(func):
|
def add_param(func):
|
||||||
|
if not func:
|
||||||
|
return func
|
||||||
|
|
||||||
if '__api_path_params' not in dir(func):
|
if '__api_path_params' not in dir(func):
|
||||||
func.__api_path_params = {}
|
func.__api_path_params = {}
|
||||||
func.__api_path_params[name] = {
|
func.__api_path_params[name] = {
|
||||||
|
@ -346,6 +352,25 @@ def log_action(kind, user_or_orgname, metadata=None, repo=None):
|
||||||
metadata=metadata, repository=repo)
|
metadata=metadata, repository=repo)
|
||||||
|
|
||||||
|
|
||||||
|
def define_json_response(schema_name):
|
||||||
|
def wrapper(func):
|
||||||
|
@add_method_metadata('response_schema', schema_name)
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped(self, *args, **kwargs):
|
||||||
|
schema = self.schemas[schema_name]
|
||||||
|
resp = func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
if app.config['TESTING']:
|
||||||
|
try:
|
||||||
|
validate(resp, schema)
|
||||||
|
except ValidationError as ex:
|
||||||
|
raise InvalidResponse(ex.message)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
return wrapped
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
import endpoints.api.billing
|
import endpoints.api.billing
|
||||||
import endpoints.api.build
|
import endpoints.api.build
|
||||||
import endpoints.api.discovery
|
import endpoints.api.discovery
|
||||||
|
|
|
@ -4,10 +4,11 @@ from flask import request
|
||||||
from app import billing
|
from app import billing
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
||||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
related_user_resource, internal_only, Unauthorized, NotFound,
|
||||||
require_user_admin, show_if, hide_if, abort)
|
require_user_admin, show_if, hide_if, path_param, require_scope, abort)
|
||||||
from endpoints.api.subscribe import subscribe, subscription_view
|
from endpoints.api.subscribe import subscribe, subscription_view
|
||||||
from auth.permissions import AdministerOrganizationPermission
|
from auth.permissions import AdministerOrganizationPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from auth import scopes
|
||||||
from data import model
|
from data import model
|
||||||
from data.billing import PLANS
|
from data.billing import PLANS
|
||||||
|
|
||||||
|
@ -149,6 +150,7 @@ class UserCard(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/card')
|
@resource('/v1/organization/<orgname>/card')
|
||||||
|
@path_param('orgname', 'The name of the organization')
|
||||||
@internal_only
|
@internal_only
|
||||||
@related_user_resource(UserCard)
|
@related_user_resource(UserCard)
|
||||||
@show_if(features.BILLING)
|
@show_if(features.BILLING)
|
||||||
|
@ -171,6 +173,7 @@ class OrganizationCard(ApiResource):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrgCard')
|
@nickname('getOrgCard')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" Get the organization's credit card. """
|
""" Get the organization's credit card. """
|
||||||
|
@ -259,6 +262,7 @@ class UserPlan(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/plan')
|
@resource('/v1/organization/<orgname>/plan')
|
||||||
|
@path_param('orgname', 'The name of the organization')
|
||||||
@internal_only
|
@internal_only
|
||||||
@related_user_resource(UserPlan)
|
@related_user_resource(UserPlan)
|
||||||
@show_if(features.BILLING)
|
@show_if(features.BILLING)
|
||||||
|
@ -285,6 +289,7 @@ class OrganizationPlan(ApiResource):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('updateOrgSubscription')
|
@nickname('updateOrgSubscription')
|
||||||
@validate_json_request('OrgSubscription')
|
@validate_json_request('OrgSubscription')
|
||||||
def put(self, orgname):
|
def put(self, orgname):
|
||||||
|
@ -299,6 +304,7 @@ class OrganizationPlan(ApiResource):
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrgSubscription')
|
@nickname('getOrgSubscription')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" Fetch any existing subscription for the org. """
|
""" Fetch any existing subscription for the org. """
|
||||||
|
@ -343,11 +349,12 @@ class UserInvoiceList(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/invoices')
|
@resource('/v1/organization/<orgname>/invoices')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
@related_user_resource(UserInvoiceList)
|
@related_user_resource(UserInvoiceList)
|
||||||
@show_if(features.BILLING)
|
@show_if(features.BILLING)
|
||||||
class OrgnaizationInvoiceList(ApiResource):
|
class OrgnaizationInvoiceList(ApiResource):
|
||||||
""" Resource for listing an orgnaization's invoices. """
|
""" Resource for listing an orgnaization's invoices. """
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('listOrgInvoices')
|
@nickname('listOrgInvoices')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" List the invoices for the specified orgnaization. """
|
""" List the invoices for the specified orgnaization. """
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
from flask import request, redirect
|
from flask import request, redirect
|
||||||
|
|
||||||
from app import app, userfiles as user_files, build_logs, log_archive
|
from app import app, userfiles as user_files, build_logs, log_archive
|
||||||
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
||||||
require_repo_read, require_repo_write, validate_json_request,
|
require_repo_read, require_repo_write, validate_json_request,
|
||||||
ApiResource, internal_only, format_date, api, Unauthorized, NotFound)
|
ApiResource, internal_only, format_date, api, Unauthorized, NotFound,
|
||||||
|
path_param)
|
||||||
from endpoints.common import start_build
|
from endpoints.common import start_build
|
||||||
from endpoints.trigger import BuildTrigger
|
from endpoints.trigger import BuildTrigger
|
||||||
from data import model
|
from data import model, database
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission
|
from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission
|
||||||
from data.buildlogs import BuildStatusRetrievalError
|
from data.buildlogs import BuildStatusRetrievalError
|
||||||
|
@ -65,6 +68,13 @@ def build_status_view(build_obj, can_write=False):
|
||||||
status = {}
|
status = {}
|
||||||
phase = 'cannot_load'
|
phase = 'cannot_load'
|
||||||
|
|
||||||
|
# If the status contains a heartbeat, then check to see if has been written in the last few
|
||||||
|
# minutes. If not, then the build timed out.
|
||||||
|
if status is not None and 'heartbeat' in status and status['heartbeat']:
|
||||||
|
heartbeat = datetime.datetime.fromtimestamp(status['heartbeat'])
|
||||||
|
if datetime.datetime.now() - heartbeat > datetime.timedelta(minutes=1):
|
||||||
|
phase = database.BUILD_PHASE.INTERNAL_ERROR
|
||||||
|
|
||||||
logger.debug('Can write: %s job_config: %s', can_write, build_obj.job_config)
|
logger.debug('Can write: %s job_config: %s', can_write, build_obj.job_config)
|
||||||
resp = {
|
resp = {
|
||||||
'id': build_obj.uuid,
|
'id': build_obj.uuid,
|
||||||
|
@ -86,6 +96,7 @@ def build_status_view(build_obj, can_write=False):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/build/')
|
@resource('/v1/repository/<repopath:repository>/build/')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class RepositoryBuildList(RepositoryParamResource):
|
class RepositoryBuildList(RepositoryParamResource):
|
||||||
""" Resource related to creating and listing repository builds. """
|
""" Resource related to creating and listing repository builds. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -190,6 +201,8 @@ class RepositoryBuildList(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/status')
|
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/status')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('build_uuid', 'The UUID of the build')
|
||||||
class RepositoryBuildStatus(RepositoryParamResource):
|
class RepositoryBuildStatus(RepositoryParamResource):
|
||||||
""" Resource for dealing with repository build status. """
|
""" Resource for dealing with repository build status. """
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
|
@ -206,6 +219,8 @@ class RepositoryBuildStatus(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/logs')
|
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/logs')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('build_uuid', 'The UUID of the build')
|
||||||
class RepositoryBuildLogs(RepositoryParamResource):
|
class RepositoryBuildLogs(RepositoryParamResource):
|
||||||
""" Resource for loading repository build logs. """
|
""" Resource for loading repository build logs. """
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
|
|
|
@ -94,13 +94,19 @@ def swagger_route_data(include_internal=False, compact=False):
|
||||||
|
|
||||||
new_operation = {
|
new_operation = {
|
||||||
'method': method_name,
|
'method': method_name,
|
||||||
'nickname': method_metadata(method, 'nickname')
|
'nickname': method_metadata(method, 'nickname') or '(unnamed)'
|
||||||
}
|
}
|
||||||
|
|
||||||
if not compact:
|
if not compact:
|
||||||
|
response_type = 'void'
|
||||||
|
res_schema_name = method_metadata(method, 'response_schema')
|
||||||
|
if res_schema_name:
|
||||||
|
models[res_schema_name] = view_class.schemas[res_schema_name]
|
||||||
|
response_type = res_schema_name
|
||||||
|
|
||||||
new_operation.update({
|
new_operation.update({
|
||||||
'type': 'void',
|
'type': response_type,
|
||||||
'summary': method.__doc__ if method.__doc__ else '',
|
'summary': method.__doc__.strip() if method.__doc__ else '',
|
||||||
'parameters': parameters,
|
'parameters': parameters,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -134,7 +140,7 @@ def swagger_route_data(include_internal=False, compact=False):
|
||||||
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
|
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
|
||||||
new_resource = {
|
new_resource = {
|
||||||
'path': swagger_path,
|
'path': swagger_path,
|
||||||
'description': view_class.__doc__ if view_class.__doc__ else "",
|
'description': view_class.__doc__.strip() if view_class.__doc__ else "",
|
||||||
'operations': operations,
|
'operations': operations,
|
||||||
'name': fully_qualified_name(view_class),
|
'name': fully_qualified_name(view_class),
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ from collections import defaultdict
|
||||||
|
|
||||||
from app import storage as store
|
from app import storage as store
|
||||||
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
|
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
|
||||||
format_date, NotFound)
|
format_date, NotFound, path_param)
|
||||||
from data import model
|
from data import model
|
||||||
from util.cache import cache_control_flask_restful
|
from util.cache import cache_control_flask_restful
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ def image_view(image, image_map):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/image/')
|
@resource('/v1/repository/<repopath:repository>/image/')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class RepositoryImageList(RepositoryParamResource):
|
class RepositoryImageList(RepositoryParamResource):
|
||||||
""" Resource for listing repository images. """
|
""" Resource for listing repository images. """
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
|
@ -67,6 +68,8 @@ class RepositoryImageList(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/image/<image_id>')
|
@resource('/v1/repository/<repopath:repository>/image/<image_id>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('image_id', 'The Docker image ID')
|
||||||
class RepositoryImage(RepositoryParamResource):
|
class RepositoryImage(RepositoryParamResource):
|
||||||
""" Resource for handling repository images. """
|
""" Resource for handling repository images. """
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
|
@ -86,6 +89,8 @@ class RepositoryImage(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
|
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('image_id', 'The Docker image ID')
|
||||||
class RepositoryImageChanges(RepositoryParamResource):
|
class RepositoryImageChanges(RepositoryParamResource):
|
||||||
""" Resource for handling repository image change lists. """
|
""" Resource for handling repository image change lists. """
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,11 @@ from datetime import datetime, timedelta
|
||||||
from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args,
|
from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args,
|
||||||
RepositoryParamResource, require_repo_admin, related_user_resource,
|
RepositoryParamResource, require_repo_admin, related_user_resource,
|
||||||
format_date, Unauthorized, NotFound, require_user_admin,
|
format_date, Unauthorized, NotFound, require_user_admin,
|
||||||
internal_only)
|
internal_only, path_param, require_scope)
|
||||||
from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission
|
from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from data import model
|
from data import model
|
||||||
|
from auth import scopes
|
||||||
|
|
||||||
|
|
||||||
def log_view(log):
|
def log_view(log):
|
||||||
|
@ -63,7 +64,7 @@ def get_logs(start_time, end_time, performer_name=None, repository=None, namespa
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/logs')
|
@resource('/v1/repository/<repopath:repository>/logs')
|
||||||
@internal_only
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class RepositoryLogs(RepositoryParamResource):
|
class RepositoryLogs(RepositoryParamResource):
|
||||||
""" Resource for fetching logs for the specific repository. """
|
""" Resource for fetching logs for the specific repository. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
|
@ -103,7 +104,7 @@ class UserLogs(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/logs')
|
@resource('/v1/organization/<orgname>/logs')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
@related_user_resource(UserLogs)
|
@related_user_resource(UserLogs)
|
||||||
class OrgLogs(ApiResource):
|
class OrgLogs(ApiResource):
|
||||||
""" Resource for fetching logs for the entire organization. """
|
""" Resource for fetching logs for the entire organization. """
|
||||||
|
@ -112,6 +113,7 @@ class OrgLogs(ApiResource):
|
||||||
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
|
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||||
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
|
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||||
@query_param('performer', 'Username for which to filter logs.', type=str)
|
@query_param('performer', 'Username for which to filter logs.', type=str)
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
def get(self, args, orgname):
|
def get(self, args, orgname):
|
||||||
""" List the logs for the specified organization. """
|
""" List the logs for the specified organization. """
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
|
|
@ -2,18 +2,19 @@ import logging
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from app import billing as stripe
|
from app import billing as stripe, avatar
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||||
related_user_resource, internal_only, Unauthorized, NotFound,
|
related_user_resource, internal_only, Unauthorized, NotFound,
|
||||||
require_user_admin, log_action, show_if)
|
require_user_admin, log_action, show_if, path_param,
|
||||||
|
require_scope)
|
||||||
from endpoints.api.team import team_view
|
from endpoints.api.team import team_view
|
||||||
from endpoints.api.user import User, PrivateRepositories
|
from endpoints.api.user import User, PrivateRepositories
|
||||||
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
|
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
|
||||||
CreateRepositoryPermission)
|
CreateRepositoryPermission)
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from auth import scopes
|
||||||
from data import model
|
from data import model
|
||||||
from data.billing import get_plan
|
from data.billing import get_plan
|
||||||
from util.gravatar import compute_hash
|
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ def org_view(o, teams):
|
||||||
view = {
|
view = {
|
||||||
'name': o.username,
|
'name': o.username,
|
||||||
'email': o.email if is_admin else '',
|
'email': o.email if is_admin else '',
|
||||||
'gravatar': compute_hash(o.email),
|
'avatar': avatar.compute_hash(o.email, name=o.username),
|
||||||
'teams': {t.name : team_view(o.username, t) for t in teams},
|
'teams': {t.name : team_view(o.username, t) for t in teams},
|
||||||
'is_admin': is_admin
|
'is_admin': is_admin
|
||||||
}
|
}
|
||||||
|
@ -97,7 +98,7 @@ class OrganizationList(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>')
|
@resource('/v1/organization/<orgname>')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
@related_user_resource(User)
|
@related_user_resource(User)
|
||||||
class Organization(ApiResource):
|
class Organization(ApiResource):
|
||||||
""" Resource for managing organizations. """
|
""" Resource for managing organizations. """
|
||||||
|
@ -118,6 +119,8 @@ class Organization(ApiResource):
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrganization')
|
@nickname('getOrganization')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" Get the details for the specified organization """
|
""" Get the details for the specified organization """
|
||||||
|
@ -133,6 +136,7 @@ class Organization(ApiResource):
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('changeOrganizationDetails')
|
@nickname('changeOrganizationDetails')
|
||||||
@validate_json_request('UpdateOrg')
|
@validate_json_request('UpdateOrg')
|
||||||
def put(self, orgname):
|
def put(self, orgname):
|
||||||
|
@ -163,11 +167,14 @@ class Organization(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/private')
|
@resource('/v1/organization/<orgname>/private')
|
||||||
|
@path_param('orgname', 'The name of the organization')
|
||||||
@internal_only
|
@internal_only
|
||||||
@related_user_resource(PrivateRepositories)
|
@related_user_resource(PrivateRepositories)
|
||||||
@show_if(features.BILLING)
|
@show_if(features.BILLING)
|
||||||
class OrgPrivateRepositories(ApiResource):
|
class OrgPrivateRepositories(ApiResource):
|
||||||
""" Custom verb to compute whether additional private repositories are available. """
|
""" Custom verb to compute whether additional private repositories are available. """
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrganizationPrivateAllowed')
|
@nickname('getOrganizationPrivateAllowed')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" Return whether or not this org is allowed to create new private repositories. """
|
""" Return whether or not this org is allowed to create new private repositories. """
|
||||||
|
@ -199,9 +206,11 @@ class OrgPrivateRepositories(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/members')
|
@resource('/v1/organization/<orgname>/members')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
class OrgnaizationMemberList(ApiResource):
|
class OrgnaizationMemberList(ApiResource):
|
||||||
""" Resource for listing the members of an organization. """
|
""" Resource for listing the members of an organization. """
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrganizationMembers')
|
@nickname('getOrganizationMembers')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" List the members of the specified organization. """
|
""" List the members of the specified organization. """
|
||||||
|
@ -232,9 +241,12 @@ class OrgnaizationMemberList(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/members/<membername>')
|
@resource('/v1/organization/<orgname>/members/<membername>')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
|
@path_param('membername', 'The username of the organization member')
|
||||||
class OrganizationMember(ApiResource):
|
class OrganizationMember(ApiResource):
|
||||||
""" Resource for managing individual organization members. """
|
""" Resource for managing individual organization members. """
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrganizationMember')
|
@nickname('getOrganizationMember')
|
||||||
def get(self, orgname, membername):
|
def get(self, orgname, membername):
|
||||||
""" Get information on the specific orgnaization member. """
|
""" Get information on the specific orgnaization member. """
|
||||||
|
@ -265,8 +277,10 @@ class OrganizationMember(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/app/<client_id>')
|
@resource('/v1/app/<client_id>')
|
||||||
|
@path_param('client_id', 'The OAuth client ID')
|
||||||
class ApplicationInformation(ApiResource):
|
class ApplicationInformation(ApiResource):
|
||||||
""" Resource that returns public information about a registered application. """
|
""" Resource that returns public information about a registered application. """
|
||||||
|
|
||||||
@nickname('getApplicationInformation')
|
@nickname('getApplicationInformation')
|
||||||
def get(self, client_id):
|
def get(self, client_id):
|
||||||
""" Get information on the specified application. """
|
""" Get information on the specified application. """
|
||||||
|
@ -274,14 +288,16 @@ class ApplicationInformation(ApiResource):
|
||||||
if not application:
|
if not application:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
org_hash = compute_hash(application.organization.email)
|
org_hash = avatar.compute_hash(application.organization.email,
|
||||||
gravatar = compute_hash(application.gravatar_email) if application.gravatar_email else org_hash
|
name=application.organization.username)
|
||||||
|
app_hash = (avatar.compute_hash(application.avatar_email, name=application.name) if
|
||||||
|
application.avatar_email else org_hash)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'name': application.name,
|
'name': application.name,
|
||||||
'description': application.description,
|
'description': application.description,
|
||||||
'uri': application.application_uri,
|
'uri': application.application_uri,
|
||||||
'gravatar': gravatar,
|
'avatar': app_hash,
|
||||||
'organization': org_view(application.organization, [])
|
'organization': org_view(application.organization, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,12 +313,12 @@ def app_view(application):
|
||||||
'client_id': application.client_id,
|
'client_id': application.client_id,
|
||||||
'client_secret': application.client_secret if is_admin else None,
|
'client_secret': application.client_secret if is_admin else None,
|
||||||
'redirect_uri': application.redirect_uri if is_admin else None,
|
'redirect_uri': application.redirect_uri if is_admin else None,
|
||||||
'gravatar_email': application.gravatar_email if is_admin else None,
|
'avatar_email': application.avatar_email if is_admin else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/applications')
|
@resource('/v1/organization/<orgname>/applications')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
class OrganizationApplications(ApiResource):
|
class OrganizationApplications(ApiResource):
|
||||||
""" Resource for managing applications defined by an organizations. """
|
""" Resource for managing applications defined by an organizations. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -330,15 +346,15 @@ class OrganizationApplications(ApiResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The human-readable description for the application',
|
'description': 'The human-readable description for the application',
|
||||||
},
|
},
|
||||||
'gravatar_email': {
|
'avatar_email': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The e-mail address of the gravatar to use for the application',
|
'description': 'The e-mail address of the avatar to use for the application',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrganizationApplications')
|
@nickname('getOrganizationApplications')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" List the applications for the specified organization """
|
""" List the applications for the specified organization """
|
||||||
|
@ -354,6 +370,7 @@ class OrganizationApplications(ApiResource):
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('createOrganizationApplication')
|
@nickname('createOrganizationApplication')
|
||||||
@validate_json_request('NewApp')
|
@validate_json_request('NewApp')
|
||||||
def post(self, orgname):
|
def post(self, orgname):
|
||||||
|
@ -371,7 +388,7 @@ class OrganizationApplications(ApiResource):
|
||||||
app_data.get('application_uri', ''),
|
app_data.get('application_uri', ''),
|
||||||
app_data.get('redirect_uri', ''),
|
app_data.get('redirect_uri', ''),
|
||||||
description = app_data.get('description', ''),
|
description = app_data.get('description', ''),
|
||||||
gravatar_email = app_data.get('gravatar_email', None),)
|
avatar_email = app_data.get('avatar_email', None),)
|
||||||
|
|
||||||
|
|
||||||
app_data.update({
|
app_data.update({
|
||||||
|
@ -386,7 +403,8 @@ class OrganizationApplications(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/applications/<client_id>')
|
@resource('/v1/organization/<orgname>/applications/<client_id>')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
|
@path_param('client_id', 'The OAuth client ID')
|
||||||
class OrganizationApplicationResource(ApiResource):
|
class OrganizationApplicationResource(ApiResource):
|
||||||
""" Resource for managing an application defined by an organizations. """
|
""" Resource for managing an application defined by an organizations. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -416,14 +434,15 @@ class OrganizationApplicationResource(ApiResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The human-readable description for the application',
|
'description': 'The human-readable description for the application',
|
||||||
},
|
},
|
||||||
'gravatar_email': {
|
'avatar_email': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The e-mail address of the gravatar to use for the application',
|
'description': 'The e-mail address of the avatar to use for the application',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrganizationApplication')
|
@nickname('getOrganizationApplication')
|
||||||
def get(self, orgname, client_id):
|
def get(self, orgname, client_id):
|
||||||
""" Retrieves the application with the specified client_id under the specified organization """
|
""" Retrieves the application with the specified client_id under the specified organization """
|
||||||
|
@ -442,6 +461,7 @@ class OrganizationApplicationResource(ApiResource):
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('updateOrganizationApplication')
|
@nickname('updateOrganizationApplication')
|
||||||
@validate_json_request('UpdateApp')
|
@validate_json_request('UpdateApp')
|
||||||
def put(self, orgname, client_id):
|
def put(self, orgname, client_id):
|
||||||
|
@ -462,7 +482,7 @@ class OrganizationApplicationResource(ApiResource):
|
||||||
application.application_uri = app_data['application_uri']
|
application.application_uri = app_data['application_uri']
|
||||||
application.redirect_uri = app_data['redirect_uri']
|
application.redirect_uri = app_data['redirect_uri']
|
||||||
application.description = app_data.get('description', '')
|
application.description = app_data.get('description', '')
|
||||||
application.gravatar_email = app_data.get('gravatar_email', None)
|
application.avatar_email = app_data.get('avatar_email', None)
|
||||||
application.save()
|
application.save()
|
||||||
|
|
||||||
app_data.update({
|
app_data.update({
|
||||||
|
@ -475,7 +495,7 @@ class OrganizationApplicationResource(ApiResource):
|
||||||
return app_view(application)
|
return app_view(application)
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('deleteOrganizationApplication')
|
@nickname('deleteOrganizationApplication')
|
||||||
def delete(self, orgname, client_id):
|
def delete(self, orgname, client_id):
|
||||||
""" Deletes the application under this organization. """
|
""" Deletes the application under this organization. """
|
||||||
|
@ -498,6 +518,8 @@ class OrganizationApplicationResource(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/applications/<client_id>/resetclientsecret')
|
@resource('/v1/organization/<orgname>/applications/<client_id>/resetclientsecret')
|
||||||
|
@path_param('orgname', 'The name of the organization')
|
||||||
|
@path_param('client_id', 'The OAuth client ID')
|
||||||
@internal_only
|
@internal_only
|
||||||
class OrganizationApplicationResetClientSecret(ApiResource):
|
class OrganizationApplicationResetClientSecret(ApiResource):
|
||||||
""" Custom verb for resetting the client secret of an application. """
|
""" Custom verb for resetting the client secret of an application. """
|
||||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||||
log_action, request_error, validate_json_request)
|
log_action, request_error, validate_json_request, path_param)
|
||||||
from data import model
|
from data import model
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ def wrap_role_view_org(role_json, user, org_members):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/permissions/team/')
|
@resource('/v1/repository/<repopath:repository>/permissions/team/')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class RepositoryTeamPermissionList(RepositoryParamResource):
|
class RepositoryTeamPermissionList(RepositoryParamResource):
|
||||||
""" Resource for repository team permissions. """
|
""" Resource for repository team permissions. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
|
@ -41,6 +42,7 @@ class RepositoryTeamPermissionList(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/permissions/user/')
|
@resource('/v1/repository/<repopath:repository>/permissions/user/')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class RepositoryUserPermissionList(RepositoryParamResource):
|
class RepositoryUserPermissionList(RepositoryParamResource):
|
||||||
""" Resource for repository user permissions. """
|
""" Resource for repository user permissions. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
|
@ -80,6 +82,8 @@ class RepositoryUserPermissionList(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/permissions/user/<username>')
|
@resource('/v1/repository/<repopath:repository>/permissions/user/<username>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('username', 'The username of the user to which the permission applies')
|
||||||
class RepositoryUserPermission(RepositoryParamResource):
|
class RepositoryUserPermission(RepositoryParamResource):
|
||||||
""" Resource for managing individual user permissions. """
|
""" Resource for managing individual user permissions. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -175,6 +179,8 @@ class RepositoryUserPermission(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/permissions/team/<teamname>')
|
@resource('/v1/repository/<repopath:repository>/permissions/team/<teamname>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('teamname', 'The name of the team to which the permission applies')
|
||||||
class RepositoryTeamPermission(RepositoryParamResource):
|
class RepositoryTeamPermission(RepositoryParamResource):
|
||||||
""" Resource for managing individual team permissions. """
|
""" Resource for managing individual team permissions. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||||
log_action, Unauthorized, NotFound, internal_only)
|
log_action, Unauthorized, NotFound, internal_only, path_param,
|
||||||
|
require_scope)
|
||||||
from auth.permissions import AdministerOrganizationPermission
|
from auth.permissions import AdministerOrganizationPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from auth import scopes
|
||||||
from data import model
|
from data import model
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,7 +56,7 @@ def log_prototype_action(action_kind, orgname, prototype, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/prototypes')
|
@resource('/v1/organization/<orgname>/prototypes')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
class PermissionPrototypeList(ApiResource):
|
class PermissionPrototypeList(ApiResource):
|
||||||
""" Resource for listing and creating permission prototypes. """
|
""" Resource for listing and creating permission prototypes. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -115,6 +117,7 @@ class PermissionPrototypeList(ApiResource):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('getOrganizationPrototypePermissions')
|
@nickname('getOrganizationPrototypePermissions')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
""" List the existing prototypes for this organization. """
|
""" List the existing prototypes for this organization. """
|
||||||
|
@ -131,6 +134,7 @@ class PermissionPrototypeList(ApiResource):
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('createOrganizationPrototypePermission')
|
@nickname('createOrganizationPrototypePermission')
|
||||||
@validate_json_request('NewPrototype')
|
@validate_json_request('NewPrototype')
|
||||||
def post(self, orgname):
|
def post(self, orgname):
|
||||||
|
@ -179,7 +183,8 @@ class PermissionPrototypeList(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/prototypes/<prototypeid>')
|
@resource('/v1/organization/<orgname>/prototypes/<prototypeid>')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
|
@path_param('prototypeid', 'The ID of the prototype')
|
||||||
class PermissionPrototype(ApiResource):
|
class PermissionPrototype(ApiResource):
|
||||||
""" Resource for managingin individual permission prototypes. """
|
""" Resource for managingin individual permission prototypes. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -204,6 +209,7 @@ class PermissionPrototype(ApiResource):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('deleteOrganizationPrototypePermission')
|
@nickname('deleteOrganizationPrototypePermission')
|
||||||
def delete(self, orgname, prototypeid):
|
def delete(self, orgname, prototypeid):
|
||||||
""" Delete an existing permission prototype. """
|
""" Delete an existing permission prototype. """
|
||||||
|
@ -224,6 +230,7 @@ class PermissionPrototype(ApiResource):
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('updateOrganizationPrototypePermission')
|
@nickname('updateOrganizationPrototypePermission')
|
||||||
@validate_json_request('PrototypeUpdate')
|
@validate_json_request('PrototypeUpdate')
|
||||||
def put(self, orgname, prototypeid):
|
def put(self, orgname, prototypeid):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask import request, abort
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||||
log_action, validate_json_request, NotFound, internal_only,
|
log_action, validate_json_request, NotFound, internal_only,
|
||||||
show_if)
|
path_param, show_if)
|
||||||
|
|
||||||
from app import tf
|
from app import tf
|
||||||
from data import model
|
from data import model
|
||||||
|
@ -28,6 +28,8 @@ def record_view(record):
|
||||||
@internal_only
|
@internal_only
|
||||||
@show_if(features.MAILING)
|
@show_if(features.MAILING)
|
||||||
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('email', 'The e-mail address')
|
||||||
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
||||||
""" Resource for checking and authorizing e-mail addresses to receive repo notifications. """
|
""" Resource for checking and authorizing e-mail addresses to receive repo notifications. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
|
|
|
@ -7,7 +7,9 @@ from data import model
|
||||||
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
||||||
require_repo_read, require_repo_write, require_repo_admin,
|
require_repo_read, require_repo_write, require_repo_admin,
|
||||||
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
|
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
|
||||||
request_error, require_scope, Unauthorized, NotFound, InvalidRequest)
|
request_error, require_scope, Unauthorized, NotFound, InvalidRequest,
|
||||||
|
path_param)
|
||||||
|
|
||||||
from auth.permissions import (ModifyRepositoryPermission, AdministerRepositoryPermission,
|
from auth.permissions import (ModifyRepositoryPermission, AdministerRepositoryPermission,
|
||||||
CreateRepositoryPermission, ReadRepositoryPermission)
|
CreateRepositoryPermission, ReadRepositoryPermission)
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
@ -140,6 +142,7 @@ class RepositoryList(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>')
|
@resource('/v1/repository/<repopath:repository>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class Repository(RepositoryParamResource):
|
class Repository(RepositoryParamResource):
|
||||||
"""Operations for managing a specific repository."""
|
"""Operations for managing a specific repository."""
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -232,6 +235,7 @@ class Repository(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/changevisibility')
|
@resource('/v1/repository/<repopath:repository>/changevisibility')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class RepositoryVisibility(RepositoryParamResource):
|
class RepositoryVisibility(RepositoryParamResource):
|
||||||
""" Custom verb for changing the visibility of the repository. """
|
""" Custom verb for changing the visibility of the repository. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
|
|
@ -4,7 +4,8 @@ from flask import request, abort
|
||||||
|
|
||||||
from app import notification_queue
|
from app import notification_queue
|
||||||
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
||||||
log_action, validate_json_request, api, NotFound, request_error)
|
log_action, validate_json_request, api, NotFound, request_error,
|
||||||
|
path_param)
|
||||||
from endpoints.notificationevent import NotificationEvent
|
from endpoints.notificationevent import NotificationEvent
|
||||||
from endpoints.notificationmethod import (NotificationMethod,
|
from endpoints.notificationmethod import (NotificationMethod,
|
||||||
CannotValidateNotificationMethodException)
|
CannotValidateNotificationMethodException)
|
||||||
|
@ -28,6 +29,7 @@ def notification_view(notification):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/notification/')
|
@resource('/v1/repository/<repopath:repository>/notification/')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class RepositoryNotificationList(RepositoryParamResource):
|
class RepositoryNotificationList(RepositoryParamResource):
|
||||||
""" Resource for dealing with listing and creating notifications on a repository. """
|
""" Resource for dealing with listing and creating notifications on a repository. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -95,6 +97,8 @@ class RepositoryNotificationList(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/notification/<uuid>')
|
@resource('/v1/repository/<repopath:repository>/notification/<uuid>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('uuid', 'The UUID of the notification')
|
||||||
class RepositoryNotification(RepositoryParamResource):
|
class RepositoryNotification(RepositoryParamResource):
|
||||||
""" Resource for dealing with specific notifications. """
|
""" Resource for dealing with specific notifications. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
|
@ -126,6 +130,8 @@ class RepositoryNotification(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/notification/<uuid>/test')
|
@resource('/v1/repository/<repopath:repository>/notification/<uuid>/test')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('uuid', 'The UUID of the notification')
|
||||||
class TestRepositoryNotification(RepositoryParamResource):
|
class TestRepositoryNotification(RepositoryParamResource):
|
||||||
""" Resource for queuing a test of a notification. """
|
""" Resource for queuing a test of a notification. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
|
||||||
log_action, validate_json_request, NotFound)
|
log_action, validate_json_request, NotFound, path_param)
|
||||||
from data import model
|
from data import model
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ def token_view(token_obj):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/tokens/')
|
@resource('/v1/repository/<repopath:repository>/tokens/')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class RepositoryTokenList(RepositoryParamResource):
|
class RepositoryTokenList(RepositoryParamResource):
|
||||||
""" Resource for creating and listing repository tokens. """
|
""" Resource for creating and listing repository tokens. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -66,6 +67,8 @@ class RepositoryTokenList(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/tokens/<code>')
|
@resource('/v1/repository/<repopath:repository>/tokens/<code>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('code', 'The token code')
|
||||||
class RepositoryToken(RepositoryParamResource):
|
class RepositoryToken(RepositoryParamResource):
|
||||||
""" Resource for managing individual tokens. """
|
""" Resource for managing individual tokens. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
|
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
|
||||||
require_scope)
|
require_scope, path_param)
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
|
from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
|
||||||
ReadRepositoryPermission, UserAdminPermission,
|
ReadRepositoryPermission, UserAdminPermission,
|
||||||
AdministerOrganizationPermission)
|
AdministerOrganizationPermission)
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from util.gravatar import compute_hash
|
from app import avatar
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/entities/<prefix>')
|
@resource('/v1/entities/<prefix>')
|
||||||
class EntitySearch(ApiResource):
|
class EntitySearch(ApiResource):
|
||||||
""" Resource for searching entities. """
|
""" Resource for searching entities. """
|
||||||
|
@path_param('prefix', 'The prefix of the entities being looked up')
|
||||||
@parse_args
|
@parse_args
|
||||||
@query_param('namespace', 'Namespace to use when querying for org entities.', type=str,
|
@query_param('namespace', 'Namespace to use when querying for org entities.', type=str,
|
||||||
default='')
|
default='')
|
||||||
|
@ -44,7 +45,7 @@ class EntitySearch(ApiResource):
|
||||||
'name': namespace_name,
|
'name': namespace_name,
|
||||||
'kind': 'org',
|
'kind': 'org',
|
||||||
'is_org_member': True,
|
'is_org_member': True,
|
||||||
'gravatar': compute_hash(organization.email),
|
'avatar': avatar.compute_hash(organization.email, name=organization.username),
|
||||||
}]
|
}]
|
||||||
|
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
|
|
|
@ -9,7 +9,7 @@ from flask import request
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
log_action, internal_only, NotFound, require_user_admin, format_date,
|
log_action, internal_only, NotFound, require_user_admin, format_date,
|
||||||
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
|
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
|
||||||
query_param, abort, require_fresh_login)
|
query_param, abort, require_fresh_login, path_param)
|
||||||
|
|
||||||
from endpoints.api.logs import get_logs
|
from endpoints.api.logs import get_logs
|
||||||
|
|
||||||
|
@ -166,6 +166,7 @@ class SuperUserSendRecoveryEmail(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/superuser/users/<username>')
|
@resource('/v1/superuser/users/<username>')
|
||||||
|
@path_param('username', 'The username of the user being managed')
|
||||||
@internal_only
|
@internal_only
|
||||||
@show_if(features.SUPER_USERS)
|
@show_if(features.SUPER_USERS)
|
||||||
class SuperUserManagement(ApiResource):
|
class SuperUserManagement(ApiResource):
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||||
RepositoryParamResource, log_action, NotFound, validate_json_request)
|
RepositoryParamResource, log_action, NotFound, validate_json_request,
|
||||||
|
path_param)
|
||||||
from endpoints.api.image import image_view
|
from endpoints.api.image import image_view
|
||||||
from data import model
|
from data import model
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('tag', 'The name of the tag')
|
||||||
class RepositoryTag(RepositoryParamResource):
|
class RepositoryTag(RepositoryParamResource):
|
||||||
""" Resource for managing repository tags. """
|
""" Resource for managing repository tags. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -73,6 +76,8 @@ class RepositoryTag(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>/images')
|
@resource('/v1/repository/<repopath:repository>/tag/<tag>/images')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('tag', 'The name of the tag')
|
||||||
class RepositoryTagImages(RepositoryParamResource):
|
class RepositoryTagImages(RepositoryParamResource):
|
||||||
""" Resource for listing the images in a specific repository tag. """
|
""" Resource for listing the images in a specific repository tag. """
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
|
|
|
@ -2,13 +2,14 @@ from flask import request
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||||
log_action, Unauthorized, NotFound, internal_only, require_scope,
|
log_action, Unauthorized, NotFound, internal_only, require_scope,
|
||||||
query_param, truthy_bool, parse_args, require_user_admin, show_if)
|
path_param, query_param, truthy_bool, parse_args, require_user_admin,
|
||||||
|
show_if)
|
||||||
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from data import model
|
from data import model
|
||||||
from util.useremails import send_org_invite_email
|
from util.useremails import send_org_invite_email
|
||||||
from util.gravatar import compute_hash
|
from app import avatar
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ def member_view(member, invited=False):
|
||||||
'name': member.username,
|
'name': member.username,
|
||||||
'kind': 'user',
|
'kind': 'user',
|
||||||
'is_robot': member.robot,
|
'is_robot': member.robot,
|
||||||
'gravatar': compute_hash(member.email) if not member.robot else None,
|
'avatar': avatar.compute_hash(member.email, name=member.username) if not member.robot else None,
|
||||||
'invited': invited,
|
'invited': invited,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,13 +76,14 @@ def invite_view(invite):
|
||||||
return {
|
return {
|
||||||
'email': invite.email,
|
'email': invite.email,
|
||||||
'kind': 'invite',
|
'kind': 'invite',
|
||||||
'gravatar': compute_hash(invite.email),
|
'avatar': avatar.compute_hash(invite.email),
|
||||||
'invited': True
|
'invited': True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/team/<teamname>')
|
@resource('/v1/organization/<orgname>/team/<teamname>')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
|
@path_param('teamname', 'The name of the team')
|
||||||
class OrganizationTeam(ApiResource):
|
class OrganizationTeam(ApiResource):
|
||||||
""" Resource for manging an organization's teams. """
|
""" Resource for manging an organization's teams. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -110,6 +112,7 @@ class OrganizationTeam(ApiResource):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('updateOrganizationTeam')
|
@nickname('updateOrganizationTeam')
|
||||||
@validate_json_request('TeamDescription')
|
@validate_json_request('TeamDescription')
|
||||||
def put(self, orgname, teamname):
|
def put(self, orgname, teamname):
|
||||||
|
@ -151,6 +154,7 @@ class OrganizationTeam(ApiResource):
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('deleteOrganizationTeam')
|
@nickname('deleteOrganizationTeam')
|
||||||
def delete(self, orgname, teamname):
|
def delete(self, orgname, teamname):
|
||||||
""" Delete the specified team. """
|
""" Delete the specified team. """
|
||||||
|
@ -164,9 +168,11 @@ class OrganizationTeam(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/team/<teamname>/members')
|
@resource('/v1/organization/<orgname>/team/<teamname>/members')
|
||||||
@internal_only
|
@path_param('orgname', 'The name of the organization')
|
||||||
|
@path_param('teamname', 'The name of the team')
|
||||||
class TeamMemberList(ApiResource):
|
class TeamMemberList(ApiResource):
|
||||||
""" Resource for managing the list of members for a team. """
|
""" Resource for managing the list of members for a team. """
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@parse_args
|
@parse_args
|
||||||
@query_param('includePending', 'Whether to include pending members', type=truthy_bool, default=False)
|
@query_param('includePending', 'Whether to include pending members', type=truthy_bool, default=False)
|
||||||
@nickname('getOrganizationTeamMembers')
|
@nickname('getOrganizationTeamMembers')
|
||||||
|
@ -199,8 +205,12 @@ class TeamMemberList(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/organization/<orgname>/team/<teamname>/members/<membername>')
|
@resource('/v1/organization/<orgname>/team/<teamname>/members/<membername>')
|
||||||
|
@path_param('orgname', 'The name of the organization')
|
||||||
|
@path_param('teamname', 'The name of the team')
|
||||||
|
@path_param('membername', 'The username of the team member')
|
||||||
class TeamMember(ApiResource):
|
class TeamMember(ApiResource):
|
||||||
""" Resource for managing individual members of a team. """
|
""" Resource for managing individual members of a team. """
|
||||||
|
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname('updateOrganizationTeamMember')
|
@nickname('updateOrganizationTeamMember')
|
||||||
def put(self, orgname, teamname, membername):
|
def put(self, orgname, teamname, membername):
|
||||||
|
|
|
@ -8,7 +8,8 @@ from urlparse import urlunparse
|
||||||
from app import app
|
from app import app
|
||||||
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
||||||
log_action, request_error, query_param, parse_args, internal_only,
|
log_action, request_error, query_param, parse_args, internal_only,
|
||||||
validate_json_request, api, Unauthorized, NotFound, InvalidRequest)
|
validate_json_request, api, Unauthorized, NotFound, InvalidRequest,
|
||||||
|
path_param)
|
||||||
from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus,
|
from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus,
|
||||||
get_trigger_config)
|
get_trigger_config)
|
||||||
from endpoints.common import start_build
|
from endpoints.common import start_build
|
||||||
|
@ -30,6 +31,7 @@ def _prepare_webhook_url(scheme, username, password, hostname, path):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/')
|
@resource('/v1/repository/<repopath:repository>/trigger/')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
class BuildTriggerList(RepositoryParamResource):
|
class BuildTriggerList(RepositoryParamResource):
|
||||||
""" Resource for listing repository build triggers. """
|
""" Resource for listing repository build triggers. """
|
||||||
|
|
||||||
|
@ -44,6 +46,8 @@ class BuildTriggerList(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
class BuildTrigger(RepositoryParamResource):
|
class BuildTrigger(RepositoryParamResource):
|
||||||
""" Resource for managing specific build triggers. """
|
""" Resource for managing specific build triggers. """
|
||||||
|
|
||||||
|
@ -90,6 +94,8 @@ class BuildTrigger(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/subdir')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/subdir')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
@internal_only
|
@internal_only
|
||||||
class BuildTriggerSubdirs(RepositoryParamResource):
|
class BuildTriggerSubdirs(RepositoryParamResource):
|
||||||
""" Custom verb for fetching the subdirs which are buildable for a trigger. """
|
""" Custom verb for fetching the subdirs which are buildable for a trigger. """
|
||||||
|
@ -137,7 +143,8 @@ class BuildTriggerSubdirs(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/activate')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/activate')
|
||||||
@internal_only
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
class BuildTriggerActivate(RepositoryParamResource):
|
class BuildTriggerActivate(RepositoryParamResource):
|
||||||
""" Custom verb for activating a build trigger once all required information has been collected.
|
""" Custom verb for activating a build trigger once all required information has been collected.
|
||||||
"""
|
"""
|
||||||
|
@ -234,6 +241,8 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
@internal_only
|
@internal_only
|
||||||
class BuildTriggerAnalyze(RepositoryParamResource):
|
class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
|
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
|
||||||
|
@ -369,6 +378,8 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
class ActivateBuildTrigger(RepositoryParamResource):
|
class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
""" Custom verb to manually activate a build trigger. """
|
""" Custom verb to manually activate a build trigger. """
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -424,6 +435,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/builds')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/builds')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
class TriggerBuildList(RepositoryParamResource):
|
class TriggerBuildList(RepositoryParamResource):
|
||||||
""" Resource to represent builds that were activated from the specified trigger. """
|
""" Resource to represent builds that were activated from the specified trigger. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
|
@ -471,6 +484,8 @@ class BuildTriggerFieldValues(RepositoryParamResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
|
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
@internal_only
|
@internal_only
|
||||||
class BuildTriggerSources(RepositoryParamResource):
|
class BuildTriggerSources(RepositoryParamResource):
|
||||||
""" Custom verb to fetch the list of build sources for the trigger config. """
|
""" Custom verb to fetch the list of build sources for the trigger config. """
|
||||||
|
|
|
@ -5,11 +5,11 @@ from flask import request
|
||||||
from flask.ext.login import logout_user
|
from flask.ext.login import logout_user
|
||||||
from flask.ext.principal import identity_changed, AnonymousIdentity
|
from flask.ext.principal import identity_changed, AnonymousIdentity
|
||||||
|
|
||||||
from app import app, billing as stripe, authentication
|
from app import app, billing as stripe, authentication, avatar
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
log_action, internal_only, NotFound, require_user_admin, parse_args,
|
log_action, internal_only, NotFound, require_user_admin, parse_args,
|
||||||
query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
|
query_param, InvalidToken, require_scope, format_date, hide_if, show_if,
|
||||||
license_error, require_fresh_login)
|
license_error, require_fresh_login, path_param, define_json_response)
|
||||||
from endpoints.api.subscribe import subscribe
|
from endpoints.api.subscribe import subscribe
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
from endpoints.api.team import try_accept_invite
|
from endpoints.api.team import try_accept_invite
|
||||||
|
@ -20,7 +20,6 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository
|
||||||
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from util.gravatar import compute_hash
|
|
||||||
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email, send_password_changed)
|
from util.useremails import (send_confirmation_email, send_recovery_email, send_change_email, send_password_changed)
|
||||||
from util.names import parse_single_urn
|
from util.names import parse_single_urn
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ def user_view(user):
|
||||||
admin_org = AdministerOrganizationPermission(o.username)
|
admin_org = AdministerOrganizationPermission(o.username)
|
||||||
return {
|
return {
|
||||||
'name': o.username,
|
'name': o.username,
|
||||||
'gravatar': compute_hash(o.email),
|
'avatar': avatar.compute_hash(o.email, name=o.username),
|
||||||
'is_org_admin': admin_org.can(),
|
'is_org_admin': admin_org.can(),
|
||||||
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
|
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
|
||||||
'preferred_namespace': not (o.stripe_id is None)
|
'preferred_namespace': not (o.stripe_id is None)
|
||||||
|
@ -60,13 +59,13 @@ def user_view(user):
|
||||||
'verified': user.verified,
|
'verified': user.verified,
|
||||||
'anonymous': False,
|
'anonymous': False,
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
'email': user.email,
|
'avatar': avatar.compute_hash(user.email, name=user.username),
|
||||||
'gravatar': compute_hash(user.email),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user_admin = UserAdminPermission(user.username)
|
user_admin = UserAdminPermission(user.username)
|
||||||
if user_admin.can():
|
if user_admin.can():
|
||||||
user_response.update({
|
user_response.update({
|
||||||
|
'email': user.email,
|
||||||
'organizations': [org_view(o) for o in organizations],
|
'organizations': [org_view(o) for o in organizations],
|
||||||
'logins': [login_view(login) for login in logins],
|
'logins': [login_view(login) for login in logins],
|
||||||
'can_create_repo': True,
|
'can_create_repo': True,
|
||||||
|
@ -149,10 +148,51 @@ class User(ApiResource):
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'UserView': {
|
||||||
|
'id': 'UserView',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Describes a user',
|
||||||
|
'required': ['verified', 'anonymous', 'avatar'],
|
||||||
|
'properties': {
|
||||||
|
'verified': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'description': 'Whether the user\'s email address has been verified'
|
||||||
|
},
|
||||||
|
'anonymous': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'description': 'true if this user data represents a guest user'
|
||||||
|
},
|
||||||
|
'email': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The user\'s email address',
|
||||||
|
},
|
||||||
|
'avatar': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Avatar hash representing the user\'s icon'
|
||||||
|
},
|
||||||
|
'organizations': {
|
||||||
|
'type': 'array',
|
||||||
|
'description': 'Information about the organizations in which the user is a member'
|
||||||
|
},
|
||||||
|
'logins': {
|
||||||
|
'type': 'array',
|
||||||
|
'description': 'The list of external login providers against which the user has authenticated'
|
||||||
|
},
|
||||||
|
'can_create_repo': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'description': 'Whether the user has permission to create repositories'
|
||||||
|
},
|
||||||
|
'preferred_namespace': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'description': 'If true, the user\'s namespace is the preferred namespace to display'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@require_scope(scopes.READ_USER)
|
@require_scope(scopes.READ_USER)
|
||||||
@nickname('getLoggedInUser')
|
@nickname('getLoggedInUser')
|
||||||
|
@define_json_response('UserView')
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Get user information for the authenticated user. """
|
""" Get user information for the authenticated user. """
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
|
@ -166,6 +206,7 @@ class User(ApiResource):
|
||||||
@nickname('changeUserDetails')
|
@nickname('changeUserDetails')
|
||||||
@internal_only
|
@internal_only
|
||||||
@validate_json_request('UpdateUser')
|
@validate_json_request('UpdateUser')
|
||||||
|
@define_json_response('UserView')
|
||||||
def put(self):
|
def put(self):
|
||||||
""" Update a users details such as password or email. """
|
""" Update a users details such as password or email. """
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
|
@ -526,6 +567,7 @@ class UserNotificationList(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/user/notifications/<uuid>')
|
@resource('/v1/user/notifications/<uuid>')
|
||||||
|
@path_param('uuid', 'The uuid of the user notification')
|
||||||
@internal_only
|
@internal_only
|
||||||
class UserNotification(ApiResource):
|
class UserNotification(ApiResource):
|
||||||
schemas = {
|
schemas = {
|
||||||
|
@ -572,10 +614,12 @@ def authorization_view(access_token):
|
||||||
'name': oauth_app.name,
|
'name': oauth_app.name,
|
||||||
'description': oauth_app.description,
|
'description': oauth_app.description,
|
||||||
'url': oauth_app.application_uri,
|
'url': oauth_app.application_uri,
|
||||||
'gravatar': compute_hash(oauth_app.gravatar_email or oauth_app.organization.email),
|
'avatar': avatar.compute_hash(oauth_app.avatar_email or oauth_app.organization.email,
|
||||||
|
name=oauth_app.name),
|
||||||
'organization': {
|
'organization': {
|
||||||
'name': oauth_app.organization.username,
|
'name': oauth_app.organization.username,
|
||||||
'gravatar': compute_hash(oauth_app.organization.email)
|
'avatar': avatar.compute_hash(oauth_app.organization.email,
|
||||||
|
name=oauth_app.organization.username)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'scopes': scopes.get_scope_information(access_token.scope),
|
'scopes': scopes.get_scope_information(access_token.scope),
|
||||||
|
@ -596,6 +640,7 @@ class UserAuthorizationList(ApiResource):
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/user/authorizations/<access_token_uuid>')
|
@resource('/v1/user/authorizations/<access_token_uuid>')
|
||||||
|
@path_param('access_token_uuid', 'The uuid of the access token')
|
||||||
@internal_only
|
@internal_only
|
||||||
class UserAuthorization(ApiResource):
|
class UserAuthorization(ApiResource):
|
||||||
@require_user_admin
|
@require_user_admin
|
||||||
|
|
|
@ -198,6 +198,7 @@ def render_page_template(name, **kwargs):
|
||||||
feature_set=json.dumps(features.get_features()),
|
feature_set=json.dumps(features.get_features()),
|
||||||
config_set=json.dumps(getFrontendVisibleConfig(app.config)),
|
config_set=json.dumps(getFrontendVisibleConfig(app.config)),
|
||||||
oauth_set=json.dumps(get_oauth_config()),
|
oauth_set=json.dumps(get_oauth_config()),
|
||||||
|
scope_set=json.dumps(scopes.ALL_SCOPES),
|
||||||
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
mixpanel_key=app.config.get('MIXPANEL_KEY', ''),
|
||||||
google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''),
|
google_analytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''),
|
||||||
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
|
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
|
||||||
|
|
|
@ -150,8 +150,10 @@ def raise_unsupported():
|
||||||
class GithubBuildTrigger(BuildTrigger):
|
class GithubBuildTrigger(BuildTrigger):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_client(auth_token):
|
def _get_client(auth_token):
|
||||||
return Github(auth_token, client_id=github_trigger.client_id(),
|
return Github(auth_token,
|
||||||
client_secret=github_trigger.client_secret())
|
base_url=github_trigger.api_endpoint(),
|
||||||
|
client_id=github_trigger.client_id(),
|
||||||
|
client_secret=github_trigger.client_secret())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def service_name(cls):
|
def service_name(cls):
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
from flask import (abort, redirect, request, url_for, make_response, Response,
|
from flask import (abort, redirect, request, url_for, make_response, Response,
|
||||||
Blueprint, send_from_directory, jsonify)
|
Blueprint, send_from_directory, jsonify)
|
||||||
|
|
||||||
|
from avatar_generator import Avatar
|
||||||
from flask.ext.login import current_user
|
from flask.ext.login import current_user
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
from health.healthcheck import HealthCheck
|
from health.healthcheck import HealthCheck
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from data.model.oauth import DatabaseAuthorizationProvider
|
from data.model.oauth import DatabaseAuthorizationProvider
|
||||||
from app import app, billing as stripe, build_logs
|
from app import app, billing as stripe, build_logs, avatar
|
||||||
from auth.auth import require_session_login, process_oauth
|
from auth.auth import require_session_login, process_oauth
|
||||||
from auth.permissions import AdministerOrganizationPermission, ReadRepositoryPermission
|
from auth.permissions import AdministerOrganizationPermission, ReadRepositoryPermission
|
||||||
from util.invoice import renderInvoiceToPdf
|
from util.invoice import renderInvoiceToPdf
|
||||||
|
@ -17,8 +18,8 @@ from util.seo import render_snapshot
|
||||||
from util.cache import no_cache
|
from util.cache import no_cache
|
||||||
from endpoints.common import common_login, render_page_template, route_show_if, param_required
|
from endpoints.common import common_login, render_page_template, route_show_if, param_required
|
||||||
from endpoints.csrf import csrf_protect, generate_csrf_token
|
from endpoints.csrf import csrf_protect, generate_csrf_token
|
||||||
|
from endpoints.registry import set_cache_headers
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
from util.gravatar import compute_hash
|
|
||||||
from util.useremails import send_email_changed
|
from util.useremails import send_email_changed
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
|
||||||
|
@ -182,6 +183,20 @@ def status():
|
||||||
return response
|
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'])
|
@web.route('/tos', methods=['GET'])
|
||||||
@no_cache
|
@no_cache
|
||||||
def tos():
|
def tos():
|
||||||
|
@ -391,7 +406,7 @@ def request_authorization_code():
|
||||||
|
|
||||||
if (not current_user.is_authenticated() or
|
if (not current_user.is_authenticated() or
|
||||||
not provider.validate_has_scopes(client_id, current_user.db_user().username, scope)):
|
not provider.validate_has_scopes(client_id, current_user.db_user().username, scope)):
|
||||||
if not provider.validate_redirect_uri(client_id, redirect_uri):
|
if redirect_uri != 'display' and not provider.validate_redirect_uri(client_id, redirect_uri):
|
||||||
current_app = provider.get_application_for_client_id(client_id)
|
current_app = provider.get_application_for_client_id(client_id)
|
||||||
if not current_app:
|
if not current_app:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
@ -411,9 +426,11 @@ def request_authorization_code():
|
||||||
'name': oauth_app.name,
|
'name': oauth_app.name,
|
||||||
'description': oauth_app.description,
|
'description': oauth_app.description,
|
||||||
'url': oauth_app.application_uri,
|
'url': oauth_app.application_uri,
|
||||||
|
'avatar': avatar.compute_hash(oauth_app.avatar_email, name=oauth_app.name),
|
||||||
'organization': {
|
'organization': {
|
||||||
'name': oauth_app.organization.username,
|
'name': oauth_app.organization.username,
|
||||||
'gravatar': compute_hash(oauth_app.organization.email)
|
'avatar': avatar.compute_hash(oauth_app.organization.email,
|
||||||
|
name=oauth_app.organization.username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -431,7 +448,6 @@ def request_authorization_code():
|
||||||
else:
|
else:
|
||||||
return provider.get_authorization_code(response_type, client_id, redirect_uri, scope=scope)
|
return provider.get_authorization_code(response_type, client_id, redirect_uri, scope=scope)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/oauth/access_token', methods=['POST'])
|
@web.route('/oauth/access_token', methods=['POST'])
|
||||||
@no_cache
|
@no_cache
|
||||||
@param_required('grant_type')
|
@param_required('grant_type')
|
||||||
|
|
1
local-run.sh
Executable file
1
local-run.sh
Executable file
|
@ -0,0 +1 @@
|
||||||
|
gunicorn -c conf/gunicorn_local.py application:application
|
1
local-test.sh
Executable file
1
local-test.sh
Executable file
|
@ -0,0 +1 @@
|
||||||
|
TEST=true python -m unittest discover
|
|
@ -1,3 +1,6 @@
|
||||||
|
autobahn
|
||||||
|
aiowsgi
|
||||||
|
trollius
|
||||||
peewee
|
peewee
|
||||||
flask
|
flask
|
||||||
py-bcrypt
|
py-bcrypt
|
||||||
|
@ -20,7 +23,7 @@ redis
|
||||||
hiredis
|
hiredis
|
||||||
docker-py
|
docker-py
|
||||||
pygithub
|
pygithub
|
||||||
flask-restful
|
flask-restful==0.2.12
|
||||||
jsonschema
|
jsonschema
|
||||||
git+https://github.com/NateFerrero/oauth2lib.git
|
git+https://github.com/NateFerrero/oauth2lib.git
|
||||||
alembic
|
alembic
|
||||||
|
@ -36,4 +39,5 @@ psycopg2
|
||||||
pyyaml
|
pyyaml
|
||||||
git+https://github.com/DevTable/aniso8601-fake.git
|
git+https://github.com/DevTable/aniso8601-fake.git
|
||||||
git+https://github.com/DevTable/anunidecode.git
|
git+https://github.com/DevTable/anunidecode.git
|
||||||
|
git+https://github.com/DevTable/avatar-generator.git
|
||||||
gipc
|
gipc
|
||||||
|
|
|
@ -15,9 +15,12 @@ PyPDF2==1.23
|
||||||
PyYAML==3.11
|
PyYAML==3.11
|
||||||
SQLAlchemy==0.9.8
|
SQLAlchemy==0.9.8
|
||||||
Werkzeug==0.9.6
|
Werkzeug==0.9.6
|
||||||
|
alembic==0.7.0
|
||||||
git+https://github.com/DevTable/aniso8601-fake.git
|
git+https://github.com/DevTable/aniso8601-fake.git
|
||||||
git+https://github.com/DevTable/anunidecode.git
|
git+https://github.com/DevTable/anunidecode.git
|
||||||
alembic==0.6.7
|
git+https://github.com/DevTable/avatar-generator.git
|
||||||
|
aiowsgi==0.3
|
||||||
|
autobahn==0.9.3-3
|
||||||
backports.ssl-match-hostname==3.4.0.2
|
backports.ssl-match-hostname==3.4.0.2
|
||||||
beautifulsoup4==4.3.2
|
beautifulsoup4==4.3.2
|
||||||
blinker==1.3
|
blinker==1.3
|
||||||
|
@ -34,10 +37,10 @@ html5lib==0.999
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
jsonschema==2.4.0
|
jsonschema==2.4.0
|
||||||
marisa-trie==0.6
|
marisa-trie==0.6
|
||||||
git+https://github.com/NateFerrero/oauth2lib.git
|
|
||||||
mixpanel-py==3.2.0
|
mixpanel-py==3.2.0
|
||||||
|
git+https://github.com/NateFerrero/oauth2lib.git
|
||||||
paramiko==1.15.1
|
paramiko==1.15.1
|
||||||
peewee==2.4.2
|
peewee==2.4.3
|
||||||
psycopg2==2.5.4
|
psycopg2==2.5.4
|
||||||
py-bcrypt==0.4
|
py-bcrypt==0.4
|
||||||
pycrypto==2.6.1
|
pycrypto==2.6.1
|
||||||
|
@ -51,6 +54,7 @@ reportlab==2.7
|
||||||
requests==2.4.3
|
requests==2.4.3
|
||||||
six==1.8.0
|
six==1.8.0
|
||||||
stripe==1.19.1
|
stripe==1.19.1
|
||||||
|
trollius==1.0.3
|
||||||
tzlocal==1.1.2
|
tzlocal==1.1.2
|
||||||
websocket-client==0.21.0
|
websocket-client==0.21.0
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
gunicorn -c conf/gunicorn_local.py application:application
|
|
|
@ -864,6 +864,10 @@ i.toggle-icon:hover {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phase-icon.internalerror {
|
||||||
|
background-color: #DFFF00;
|
||||||
|
}
|
||||||
|
|
||||||
.phase-icon.waiting, .phase-icon.unpacking, .phase-icon.starting, .phase-icon.initializing {
|
.phase-icon.waiting, .phase-icon.unpacking, .phase-icon.starting, .phase-icon.initializing {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
|
@ -876,6 +880,10 @@ i.toggle-icon:hover {
|
||||||
background-color: #f0ad4e;
|
background-color: #f0ad4e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phase-icon.priming-cache {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
.phase-icon.pushing {
|
.phase-icon.pushing {
|
||||||
background-color: #5cb85c;
|
background-color: #5cb85c;
|
||||||
}
|
}
|
||||||
|
@ -4256,9 +4264,10 @@ pre.command:before {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-header > img {
|
.auth-header > .avatar {
|
||||||
float: left;
|
float: left;
|
||||||
margin-top: 8px;
|
display: inline-block;
|
||||||
|
margin-top: 12px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4819,7 +4828,7 @@ i.slack-icon {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-listing .gravatar {
|
.member-listing .avatar {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
@ -4872,3 +4881,19 @@ i.slack-icon {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress.active .progress-bar {
|
||||||
|
/* Note: There is a bug in Chrome which results in high CPU usage for active progress-bars
|
||||||
|
due to their animation. This enables the GPU for the rendering, which cuts CPU usage in
|
||||||
|
half (although it is still not great)
|
||||||
|
*/
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#gen-token table {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gen-token input[type="checkbox"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="application-info-element" style="padding-bottom: 18px">
|
<div class="application-info-element" style="padding-bottom: 18px">
|
||||||
<div class="auth-header">
|
<div class="auth-header">
|
||||||
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
|
<span class="avatar" size="48" hash="application.avatar"></span>
|
||||||
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
|
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
|
||||||
<h4>
|
<h4>
|
||||||
{{ application.organization.name }}
|
{{ application.organization.name }}
|
||||||
|
|
1
static/directives/avatar.html
Normal file
1
static/directives/avatar.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<img class="avatar-element" ng-src="{{ AvatarService.getAvatar(_hash, size) }}">
|
|
@ -9,7 +9,6 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="alert alert-danger" ng-if="error.message == 'HTTP code: 403' && getLocalPullInfo().isLocal">
|
<div class="alert alert-danger" ng-if="error.message == 'HTTP code: 403' && getLocalPullInfo().isLocal">
|
||||||
<div ng-if="getLocalPullInfo().login">
|
<div ng-if="getLocalPullInfo().login">
|
||||||
Note: The credentials <b>{{ getLocalPullInfo().login.username }}</b> for registry <b>{{ getLocalPullInfo().login.registry }}</b> cannot
|
Note: The credentials <b>{{ getLocalPullInfo().login.username }}</b> for registry <b>{{ getLocalPullInfo().login.registry }}</b> cannot
|
||||||
|
|
|
@ -7,15 +7,15 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="entity.kind == 'org'">
|
<span ng-if="entity.kind == 'org'">
|
||||||
<img ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
<span class="avatar" size="avatarSize || 16" hash="entity.avatar"></span>
|
||||||
<span class="entity-name">
|
<span class="entity-name">
|
||||||
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
|
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
|
||||||
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
|
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
|
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
|
||||||
<img class="gravatar" ng-if="showGravatar == 'true' && entity.gravatar" ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
<span class="avatar" size="avatarSize || 16" hash="entity.avatar" ng-if="showAvatar == 'true' && entity.avatar"></span>
|
||||||
<span ng-if="showGravatar != 'true' || !entity.gravatar">
|
<span ng-if="showAvatar != 'true' || !entity.avatar">
|
||||||
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
|
|
||||||
<li class="dropdown" ng-switch-when="false">
|
<li class="dropdown" ng-switch-when="false">
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
|
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
<span class="avatar" size="32" hash="user.avatar"></span>
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
<span class="notifications-bubble"></span>
|
<span class="notifications-bubble"></span>
|
||||||
<b class="caret"></b>
|
<b class="caret"></b>
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
<span class="namespace-selector-dropdown">
|
<span class="namespace-selector-dropdown">
|
||||||
<span ng-show="user.organizations.length == 0">
|
<span ng-show="user.organizations.length == 0">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
|
<span class="avatar" size="24" hash="user.avatar"></span>
|
||||||
<span class="namespace-name">{{user.username}}</span>
|
<span class="namespace-name">{{user.username}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="btn-group" ng-show="user.organizations.length > 0">
|
<div class="btn-group" ng-show="user.organizations.length > 0">
|
||||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||||
<img src="//www.gravatar.com/avatar/{{ namespaceObj.gravatar }}?s=16&d=identicon" />
|
<span class="avatar" size="16" hash="namespaceObj.avatar"></span>
|
||||||
{{namespace}} <span class="caret"></span>
|
{{namespace}} <span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
<li class="namespace-item" ng-repeat="org in user.organizations"
|
<li class="namespace-item" ng-repeat="org in user.organizations"
|
||||||
ng-class="(requireCreate && !namespaces[org.name].can_create_repo) ? 'disabled' : ''">
|
ng-class="(requireCreate && !namespaces[org.name].can_create_repo) ? 'disabled' : ''">
|
||||||
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(org)">
|
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(org)">
|
||||||
<img src="//www.gravatar.com/avatar/{{ org.gravatar }}?s=24&d=identicon" />
|
<span class="avatar" size="24" hash="org.avatar"></span>
|
||||||
<span class="namespace-name">{{ org.name }}</span>
|
<span class="namespace-name">{{ org.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li>
|
<li>
|
||||||
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(user)">
|
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(user)">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
|
<span class="avatar" size="24" hash="user.avatar"></span>
|
||||||
<span class="namespace-name">{{ user.username }}</span>
|
<span class="namespace-name">{{ user.username }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="circle" ng-class="getClass(notification)"></div>
|
<div class="circle" ng-class="getClass(notification)"></div>
|
||||||
<div class="message" ng-bind-html="getMessage(notification)"></div>
|
<div class="message" ng-bind-html="getMessage(notification)"></div>
|
||||||
<div class="orginfo" ng-if="notification.organization">
|
<div class="orginfo" ng-if="notification.organization">
|
||||||
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
|
<span class="avatar" size="24" hash="getAvatar(notification.organization)"></span>
|
||||||
<span class="orgname">{{ notification.organization }}</span>
|
<span class="orgname">{{ notification.organization }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="organization-header-element">
|
<div class="organization-header-element">
|
||||||
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&d=identicon">
|
<span class="avatar" size="24" hash="organization.avatar"></span>
|
||||||
<span class="organization-name" ng-show="teamName || clickable">
|
<span class="organization-name" ng-show="teamName || clickable">
|
||||||
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
|
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<span class="trigger-description-element" ng-switch on="trigger.service">
|
<span class="trigger-description-element" ng-switch on="trigger.service">
|
||||||
<span ng-switch-when="github">
|
<span ng-switch-when="github">
|
||||||
<i class="fa fa-github fa-lg" style="margin-right: 6px" data-title="GitHub" bs-tooltip="tooltip.title"></i>
|
<i class="fa fa-github fa-lg" style="margin-right: 6px" data-title="GitHub" bs-tooltip="tooltip.title"></i>
|
||||||
Push to GitHub repository <a href="https://github.com/{{ trigger.config.build_source }}" target="_new">{{ trigger.config.build_source }}</a>
|
Push to GitHub <span ng-if="KeyService.isEnterprise('github-trigger')">Enterprise</span> repository
|
||||||
|
<a href="{{ KeyService['githubTriggerEndpoint'] }}{{ trigger.config.build_source }}" target="_new">
|
||||||
|
{{ trigger.config.build_source }}
|
||||||
|
</a>
|
||||||
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!short">
|
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!short">
|
||||||
<div>
|
<div>
|
||||||
<span class="trigger-description-subtitle">Branches/Tags:</span>
|
<span class="trigger-description-subtitle">Branches/Tags:</span>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="current-repo">
|
<div class="current-repo">
|
||||||
<img class="dropdown-select-icon github-org-icon"
|
<img class="dropdown-select-icon github-org-icon"
|
||||||
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
|
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}">
|
||||||
<a ng-href="https://github.com/{{ state.currentRepo.repo }}" target="_blank">{{ state.currentRepo.repo }}</a>
|
<a ng-href="https://github.com/{{ state.currentRepo.repo }}" target="_blank">{{ state.currentRepo.repo }}</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<!-- Icons -->
|
<!-- Icons -->
|
||||||
<i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i>
|
<i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i>
|
||||||
<img class="dropdown-select-icon github-org-icon"
|
<img class="dropdown-select-icon github-org-icon"
|
||||||
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '//www.gravatar.com/avatar/' }}">
|
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/image/empty.png' }}">
|
||||||
|
|
||||||
<!-- Dropdown menu -->
|
<!-- Dropdown menu -->
|
||||||
<ul class="dropdown-select-menu scrollable-menu" role="menu">
|
<ul class="dropdown-select-menu scrollable-menu" role="menu">
|
||||||
|
|
BIN
static/img/empty.png
Normal file
BIN
static/img/empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 95 B |
128
static/js/app.js
128
static/js/app.js
|
@ -620,6 +620,51 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
return pingService;
|
return pingService;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
$provide.factory('AvatarService', ['Config', '$sanitize', 'md5',
|
||||||
|
function(Config, $sanitize, md5) {
|
||||||
|
var avatarService = {};
|
||||||
|
var cache = {};
|
||||||
|
|
||||||
|
avatarService.getAvatar = function(hash, opt_size) {
|
||||||
|
var size = opt_size || 16;
|
||||||
|
switch (Config['AVATAR_KIND']) {
|
||||||
|
case 'local':
|
||||||
|
return '/avatar/' + hash + '?size=' + size;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gravatar':
|
||||||
|
return '//www.gravatar.com/avatar/' + hash + '?d=identicon&size=' + size;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
avatarService.computeHash = function(opt_email, opt_name) {
|
||||||
|
var email = opt_email || '';
|
||||||
|
var name = opt_name || '';
|
||||||
|
|
||||||
|
var cacheKey = email + ':' + name;
|
||||||
|
if (!cacheKey) { return '-'; }
|
||||||
|
|
||||||
|
if (cache[cacheKey]) {
|
||||||
|
return cache[cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = md5.createHash(email.toString().toLowerCase());
|
||||||
|
switch (Config['AVATAR_KIND']) {
|
||||||
|
case 'local':
|
||||||
|
if (name) {
|
||||||
|
hash = name[0] + hash;
|
||||||
|
} else if (email) {
|
||||||
|
hash = email[0] + hash;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache[cacheKey] = hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
return avatarService;
|
||||||
|
}]);
|
||||||
|
|
||||||
$provide.factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
|
$provide.factory('TriggerService', ['UtilService', '$sanitize', 'KeyService',
|
||||||
function(UtilService, $sanitize, KeyService) {
|
function(UtilService, $sanitize, KeyService) {
|
||||||
|
@ -1712,6 +1757,12 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
return notificationService;
|
return notificationService;
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
$provide.factory('OAuthService', ['$location', 'Config', function($location, Config) {
|
||||||
|
var oauthService = {};
|
||||||
|
oauthService.SCOPES = window.__auth_scopes;
|
||||||
|
return oauthService;
|
||||||
|
}]);
|
||||||
|
|
||||||
$provide.factory('KeyService', ['$location', 'Config', function($location, Config) {
|
$provide.factory('KeyService', ['$location', 'Config', function($location, Config) {
|
||||||
var keyService = {}
|
var keyService = {}
|
||||||
var oauth = window.__oauth;
|
var oauth = window.__oauth;
|
||||||
|
@ -1730,14 +1781,22 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
|
|
||||||
keyService['githubEndpoint'] = oauth['GITHUB_LOGIN_CONFIG']['GITHUB_ENDPOINT'];
|
keyService['githubEndpoint'] = oauth['GITHUB_LOGIN_CONFIG']['GITHUB_ENDPOINT'];
|
||||||
|
|
||||||
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
|
keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT'];
|
||||||
|
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
|
||||||
|
|
||||||
keyService['githubLoginScope'] = 'user:email';
|
keyService['githubLoginScope'] = 'user:email';
|
||||||
keyService['googleLoginScope'] = 'openid email';
|
keyService['googleLoginScope'] = 'openid email';
|
||||||
|
|
||||||
keyService.isEnterprise = function(service) {
|
keyService.isEnterprise = function(service) {
|
||||||
var isGithubEnterprise = keyService['githubLoginUrl'].indexOf('https://github.com/') < 0;
|
switch (service) {
|
||||||
return service == 'github' && isGithubEnterprise;
|
case 'github':
|
||||||
|
return keyService['githubLoginUrl'].indexOf('https://github.com/') < 0;
|
||||||
|
|
||||||
|
case 'github-trigger':
|
||||||
|
return keyService['githubTriggerAuthorizeUrl'].indexOf('https://github.com/') < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
keyService.getExternalLoginUrl = function(service, action) {
|
keyService.getExternalLoginUrl = function(service, action) {
|
||||||
|
@ -2441,8 +2500,8 @@ quayApp.directive('entityReference', function () {
|
||||||
scope: {
|
scope: {
|
||||||
'entity': '=entity',
|
'entity': '=entity',
|
||||||
'namespace': '=namespace',
|
'namespace': '=namespace',
|
||||||
'showGravatar': '@showGravatar',
|
'showAvatar': '@showAvatar',
|
||||||
'gravatarSize': '@gravatarSize'
|
'avatarSize': '@avatarSize'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, UserService, UtilService) {
|
controller: function($scope, $element, UserService, UtilService) {
|
||||||
$scope.getIsAdmin = function(namespace) {
|
$scope.getIsAdmin = function(namespace) {
|
||||||
|
@ -4319,8 +4378,7 @@ quayApp.directive('entitySearch', function () {
|
||||||
} else if (datum.entity.kind == 'team') {
|
} else if (datum.entity.kind == 'team') {
|
||||||
template += '<i class="fa fa-group fa-lg"></i>';
|
template += '<i class="fa fa-group fa-lg"></i>';
|
||||||
} else if (datum.entity.kind == 'org') {
|
} else if (datum.entity.kind == 'org') {
|
||||||
template += '<i class="fa"><img src="//www.gravatar.com/avatar/' +
|
template += '<i class="fa">' + AvatarService.getAvatar(datum.entity.avatar, 16) + '</i>';
|
||||||
datum.entity.gravatar + '?s=16&d=identicon"></i>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template += '<span class="name">' + datum.value + '</span>';
|
template += '<span class="name">' + datum.value + '</span>';
|
||||||
|
@ -4747,6 +4805,11 @@ quayApp.directive('buildLogError', function () {
|
||||||
'entries': '=entries'
|
'entries': '=entries'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, Config) {
|
controller: function($scope, $element, Config) {
|
||||||
|
$scope.isInternalError = function() {
|
||||||
|
var entry = $scope.entries[$scope.entries.length - 1];
|
||||||
|
return entry && entry.data && entry.data['internal_error'];
|
||||||
|
};
|
||||||
|
|
||||||
$scope.getLocalPullInfo = function() {
|
$scope.getLocalPullInfo = function() {
|
||||||
if ($scope.entries.__localpull !== undefined) {
|
if ($scope.entries.__localpull !== undefined) {
|
||||||
return $scope.entries.__localpull;
|
return $scope.entries.__localpull;
|
||||||
|
@ -4802,7 +4865,9 @@ quayApp.directive('triggerDescription', function () {
|
||||||
'trigger': '=trigger',
|
'trigger': '=trigger',
|
||||||
'short': '=short'
|
'short': '=short'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element, KeyService, TriggerService) {
|
||||||
|
$scope.KeyService = KeyService;
|
||||||
|
$scope.TriggerService = TriggerService;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
@ -5680,6 +5745,9 @@ quayApp.directive('buildMessage', function () {
|
||||||
case 'building':
|
case 'building':
|
||||||
return 'Building image from Dockerfile';
|
return 'Building image from Dockerfile';
|
||||||
|
|
||||||
|
case 'priming-cache':
|
||||||
|
return 'Priming cache for build';
|
||||||
|
|
||||||
case 'pushing':
|
case 'pushing':
|
||||||
return 'Pushing image built from Dockerfile';
|
return 'Pushing image built from Dockerfile';
|
||||||
|
|
||||||
|
@ -5688,6 +5756,9 @@ quayApp.directive('buildMessage', function () {
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'Dockerfile build failed';
|
return 'Dockerfile build failed';
|
||||||
|
|
||||||
|
case 'internalerror':
|
||||||
|
return 'An internal system error occurred while building; the build will be retried in the next few minutes.';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -5721,6 +5792,10 @@ quayApp.directive('buildProgress', function () {
|
||||||
return buildInfo.status.push_completion * 100;
|
return buildInfo.status.push_completion * 100;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'priming-cache':
|
||||||
|
return buildInfo.status.cache_completion * 100;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'complete':
|
case 'complete':
|
||||||
return 100;
|
return 100;
|
||||||
break;
|
break;
|
||||||
|
@ -6043,9 +6118,9 @@ quayApp.directive('notificationView', function () {
|
||||||
return NotificationService.getMessage(notification);
|
return NotificationService.getMessage(notification);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getGravatar = function(orgname) {
|
$scope.getAvatar = function(orgname) {
|
||||||
var organization = UserService.getOrganization(orgname);
|
var organization = UserService.getOrganization(orgname);
|
||||||
return organization['gravatar'] || '';
|
return organization['avatar'] || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.parseDate = function(dateString) {
|
$scope.parseDate = function(dateString) {
|
||||||
|
@ -6427,6 +6502,39 @@ quayApp.directive('locationView', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
quayApp.directive('avatar', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/avatar.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: true,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'hash': '=hash',
|
||||||
|
'email': '=email',
|
||||||
|
'name': '=name',
|
||||||
|
'size': '=size'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, AvatarService) {
|
||||||
|
$scope.AvatarService = AvatarService;
|
||||||
|
|
||||||
|
var refreshHash = function() {
|
||||||
|
if (!$scope.name && !$scope.email) { return; }
|
||||||
|
$scope._hash = AvatarService.computeHash($scope.email, $scope.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('hash', function(hash) {
|
||||||
|
$scope._hash = hash;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('name', refreshHash);
|
||||||
|
$scope.$watch('email', refreshHash);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
quayApp.directive('tagSpecificImagesView', function () {
|
quayApp.directive('tagSpecificImagesView', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
|
|
@ -1331,6 +1331,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, TriggerServi
|
||||||
|
|
||||||
$scope.Features = Features;
|
$scope.Features = Features;
|
||||||
$scope.TriggerService = TriggerService;
|
$scope.TriggerService = TriggerService;
|
||||||
|
$scope.KeyService = KeyService;
|
||||||
|
|
||||||
$scope.permissions = {'team': [], 'user': [], 'loading': 2};
|
$scope.permissions = {'team': [], 'user': [], 'loading': 2};
|
||||||
$scope.logsShown = 0;
|
$scope.logsShown = 0;
|
||||||
|
@ -2696,12 +2697,28 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, ApiService) {
|
function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, OAuthService, ApiService, UserService, Config) {
|
||||||
var orgname = $routeParams.orgname;
|
var orgname = $routeParams.orgname;
|
||||||
var clientId = $routeParams.clientid;
|
var clientId = $routeParams.clientid;
|
||||||
|
|
||||||
|
$scope.Config = Config;
|
||||||
|
$scope.OAuthService = OAuthService;
|
||||||
$scope.updating = false;
|
$scope.updating = false;
|
||||||
|
|
||||||
|
$scope.genScopes = {};
|
||||||
|
|
||||||
|
UserService.updateUserIn($scope);
|
||||||
|
|
||||||
|
$scope.getScopes = function(scopes) {
|
||||||
|
var checked = [];
|
||||||
|
for (var scopeName in scopes) {
|
||||||
|
if (scopes.hasOwnProperty(scopeName) && scopes[scopeName]) {
|
||||||
|
checked.push(scopeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checked;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.askResetClientSecret = function() {
|
$scope.askResetClientSecret = function() {
|
||||||
$('#resetSecretModal').modal({});
|
$('#resetSecretModal').modal({});
|
||||||
};
|
};
|
||||||
|
@ -2736,8 +2753,8 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim
|
||||||
delete $scope.application['description'];
|
delete $scope.application['description'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$scope.application['gravatar_email']) {
|
if (!$scope.application['avatar_email']) {
|
||||||
delete $scope.application['gravatar_email'];
|
delete $scope.application['avatar_email'];
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorHandler = ApiService.errorDisplay('Could not update application', function(resp) {
|
var errorHandler = ApiService.errorDisplay('Could not update application', function(resp) {
|
||||||
|
|
|
@ -58,9 +58,6 @@
|
||||||
<small>Co-Founder</small></h3>
|
<small>Co-Founder</small></h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3 col-md-2">
|
|
||||||
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/342ea83fd68d33f90b1f06f466d533c6?s=128&d=identicon">
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-7 col-md-10">
|
<div class="col-sm-7 col-md-10">
|
||||||
<p>Jacob graduated from The University of Michigan with a Bachelors in Computer Engineering. From there he allowed his love of flight and mountains to lure him to Seattle where he took a job with Boeing Commercial Airplanes working on the world's most accurate flight simulator. When he realized how much he also loved web development, he moved to Amazon to work on the e-commerce back-end. Finally, desiring to move to New York City, he moved to Google, where he worked on several products related to Google APIs.</p>
|
<p>Jacob graduated from The University of Michigan with a Bachelors in Computer Engineering. From there he allowed his love of flight and mountains to lure him to Seattle where he took a job with Boeing Commercial Airplanes working on the world's most accurate flight simulator. When he realized how much he also loved web development, he moved to Amazon to work on the e-commerce back-end. Finally, desiring to move to New York City, he moved to Google, where he worked on several products related to Google APIs.</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,9 +68,6 @@
|
||||||
<small>Co-Founder</small></h3>
|
<small>Co-Founder</small></h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-3 col-md-2">
|
|
||||||
<img class="img-rounded founder-photo" src="http://www.gravatar.com/avatar/9fc3232622773fb2e8f71c0027601bc5?s=128&d=mm">
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-7 col-md-10">
|
<div class="col-sm-7 col-md-10">
|
||||||
<p>Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. </p>
|
<p>Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. </p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
<div class="signup-form"></div>
|
<div class="signup-form"></div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="!user.anonymous" class="user-welcome">
|
<div ng-show="!user.anonymous" class="user-welcome">
|
||||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
<span class="avatar" size="128" hash="user.avatar"></span>
|
||||||
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
||||||
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<div class="signup-form"></div>
|
<div class="signup-form"></div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="!user.anonymous" class="user-welcome">
|
<div ng-show="!user.anonymous" class="user-welcome">
|
||||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
<span class="avatar" size="128" hash="user.avatar"></span>
|
||||||
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
||||||
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
||||||
|
|
|
@ -7,10 +7,10 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="auth-header">
|
<div class="auth-header">
|
||||||
<img src="//www.gravatar.com/avatar/{{ application.gravatar_email | gravatar }}?s=48&d=identicon">
|
<span class="avatar" size="48" email="application.avatar_email" name="application.name"></span>
|
||||||
<h2>{{ application.name || '(Untitled)' }}</h2>
|
<h2>{{ application.name || '(Untitled)' }}</h2>
|
||||||
<h4>
|
<h4>
|
||||||
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&d=identicon" style="vertical-align: middle; margin-right: 4px;">
|
<span class="avatar" size="24" hash="organization.avatar" style="vertical-align: middle; margin-right: 4px;"></span>
|
||||||
<span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span>
|
<span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,6 +30,7 @@
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Settings</a></li>
|
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Settings</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#oauth">OAuth Information</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#oauth">OAuth Information</a></li>
|
||||||
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#gen-token">Generate Token</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete Application</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete Application</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,9 +60,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group nested">
|
<div class="form-group nested">
|
||||||
<label for="fieldAppGravatar">Gravatar E-mail (optional)</label>
|
<label for="fieldAppAvatar">Avatar E-mail (optional)</label>
|
||||||
<input type="email" class="form-control" id="fieldAppGravatar" placeholder="Gravatar E-mail" ng-model="application.gravatar_email">
|
<input type="email" class="form-control" id="fieldAppAvatar" placeholder="Avatar E-mail" ng-model="application.avatar_email">
|
||||||
<div class="description">An e-mail address representing the <a href="http://en.gravatar.com/" target="_blank">Gravatar</a> for the application. See above for the icon.</div>
|
<div class="description">An e-mail address representing the <a href="http://docs.quay.io/glossary/avatar" target="_blank">Avatar</a> for the application. See above for the icon.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group nested" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">
|
<div class="form-group nested" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">
|
||||||
|
@ -91,6 +92,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Generate Token tab -->
|
||||||
|
<div id="gen-token" class="tab-pane">
|
||||||
|
<div style="margin-bottom: 20px">
|
||||||
|
Click the button below to generate a new <a href="http://tools.ietf.org/html/rfc6749#section-1.4" target="_new">OAuth 2 Access Token</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 20px">
|
||||||
|
<strong>Note:</strong> The generated token will act on behalf of user
|
||||||
|
<span class="avatar" hash="user.avatar" size="16" style="margin-left: 6px; margin-right: 4px;"></span>
|
||||||
|
{{ user.username }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr ng-repeat="(scopeName, scopeInfo) in OAuthService.SCOPES">
|
||||||
|
<td><label onclick="event.stopPropagation()"><input type="checkbox" value="scopeInfo[0]" ng-model="genScopes[scopeName]">{{ scopeInfo[3] }}</label></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a class="btn btn-success"
|
||||||
|
href="{{ Config.getUrl('/oauth/authorize?response_type=token&client_id=' + application.client_id + '&scope=' + getScopes(genScopes).join(',') + '&redirect_uri=display') }}"
|
||||||
|
ng-disabled="!getScopes(genScopes).length" target="_blank">
|
||||||
|
Generate Access Token
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- OAuth tab -->
|
<!-- OAuth tab -->
|
||||||
<div id="oauth" class="tab-pane">
|
<div id="oauth" class="tab-pane">
|
||||||
<table style="margin-top: 20px;">
|
<table style="margin-top: 20px;">
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
<div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
|
<div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
|
||||||
<form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
|
<form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
|
||||||
data-content="{{ changeEmailError }}" data-placement="bottom" ng-show="!updatingOrganization">
|
data-content="{{ changeEmailError }}" data-placement="bottom" ng-show="!updatingOrganization">
|
||||||
<img src="//www.gravatar.com/avatar/{{ organizationEmail | gravatar }}?s=24&d=identicon">
|
<span class="avatar" size="24" email="organizationEmail" name="orgname"></span>
|
||||||
<input type="email" class="form-control" ng-model="organizationEmail"
|
<input type="email" class="form-control" ng-model="organizationEmail"
|
||||||
style="margin-left: 10px; margin-right: 10px; width: 400px; display: inline-block;" required>
|
style="margin-left: 10px; margin-right: 10px; width: 400px; display: inline-block;" required>
|
||||||
<button class="btn btn-primary" type="submit" ng-disabled="changeEmailForm.$invalid || organizationEmail == organization.email">
|
<button class="btn btn-primary" type="submit" ng-disabled="changeEmailForm.$invalid || organizationEmail == organization.email">
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<h2>Organizations</h2>
|
<h2>Organizations</h2>
|
||||||
|
|
||||||
<div class="organization-listing" ng-repeat="organization in user.organizations">
|
<div class="organization-listing" ng-repeat="organization in user.organizations">
|
||||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=32&d=identicon">
|
<span class="avatar" size="32" hash="organization.avatar"></span>
|
||||||
<a class="org-title" href="/organization/{{ organization.name }}">{{ organization.name }}</a>
|
<a class="org-title" href="/organization/{{ organization.name }}">{{ organization.name }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -308,7 +308,8 @@
|
||||||
<ul class="dropdown-menu dropdown-menu-right pull-right">
|
<ul class="dropdown-menu dropdown-menu-right pull-right">
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ TriggerService.getRedirectUrl('github', repo.namespace, repo.name) }}">
|
<a href="{{ TriggerService.getRedirectUrl('github', repo.namespace, repo.name) }}">
|
||||||
<i class="fa fa-github fa-lg"></i>GitHub - Repository Push
|
<i class="fa fa-github fa-lg"></i>
|
||||||
|
GitHub <span ng-if="KeyService.isEnterprise('github-trigger')">Enterprise</span> - Repository Push
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
|
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
|
||||||
<td class="user entity">
|
<td class="user entity">
|
||||||
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
|
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
||||||
|
@ -67,10 +67,10 @@
|
||||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
|
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
|
||||||
<td class="user entity">
|
<td class="user entity">
|
||||||
<span ng-if="member.kind != 'invite'">
|
<span ng-if="member.kind != 'invite'">
|
||||||
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
|
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="invite-listing" ng-if="member.kind == 'invite'">
|
<span class="invite-listing" ng-if="member.kind == 'invite'">
|
||||||
<img class="gravatar"ng-src="//www.gravatar.com/avatar/{{ member.gravatar }}?s=32&d=identicon">
|
<span class="avatar" size="32" hash="member.avatar"></span>
|
||||||
{{ member.email }}
|
{{ member.email }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="user-admin container" ng-show="!user.anonymous">
|
<div class="user-admin container" ng-show="!user.anonymous">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="organization-header-element">
|
<div class="organization-header-element">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon">
|
<span class="avatar" size="24" hash="user.avatar"></span>
|
||||||
<span class="organization-name">
|
<span class="organization-name">
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
|
|
||||||
<tr class="auth-info" ng-repeat="authInfo in authorizedApps">
|
<tr class="auth-info" ng-repeat="authInfo in authorizedApps">
|
||||||
<td>
|
<td>
|
||||||
<img src="//www.gravatar.com/avatar/{{ authInfo.gravatar }}?s=16&d=identicon">
|
<span class="avatar" size="16" hash="authInfo.application.avatar"></span>
|
||||||
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
|
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
|
||||||
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
||||||
{{ authInfo.application.name }}
|
{{ authInfo.application.name }}
|
||||||
|
@ -291,7 +291,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="orgName">Organization Name</label>
|
<label for="orgName">Organization Name</label>
|
||||||
<div class="existing-data">
|
<div class="existing-data">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon">
|
<span class="avatar" size="24" hash="user.avatar"></span>
|
||||||
{{ user.username }}</div>
|
{{ user.username }}</div>
|
||||||
<span class="description">This will continue to be the namespace for your repositories</span>
|
<span class="description">This will continue to be the namespace for your repositories</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
window.__features = {{ feature_set|safe }};
|
window.__features = {{ feature_set|safe }};
|
||||||
window.__config = {{ config_set|safe }};
|
window.__config = {{ config_set|safe }};
|
||||||
window.__oauth = {{ oauth_set|safe }};
|
window.__oauth = {{ oauth_set|safe }};
|
||||||
|
window.__auth_scopes = {{ scope_set|safe }};
|
||||||
window.__token = '{{ csrf_token() }}';
|
window.__token = '{{ csrf_token() }}';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,11 @@
|
||||||
|
|
||||||
<div class="container auth-container" ng-if="!user.anonymous">
|
<div class="container auth-container" ng-if="!user.anonymous">
|
||||||
<div class="auth-header">
|
<div class="auth-header">
|
||||||
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
|
<span class="avatar" size="48" hash="'{{ application.avatar }}'"></span>
|
||||||
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
|
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
|
||||||
<h4>
|
<h4>
|
||||||
<img src="//www.gravatar.com/avatar/{{ application.organization.gravatar }}?s=24&d=identicon" style="vertical-align: middle; margin-right: 4px;">
|
<span class="avatar" size="24" hash="'{{ application.organization.avatar }}'"
|
||||||
|
style="vertical-align: middle; margin-right: 4px;"></span>
|
||||||
<span style="vertical-align: middle">{{ application.organization.name }}</span>
|
<span style="vertical-align: middle">{{ application.organization.name }}</span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2114,14 +2114,14 @@ class TestOrganizationApplicationResource(ApiTestCase):
|
||||||
edit_json = self.putJsonResponse(OrganizationApplicationResource,
|
edit_json = self.putJsonResponse(OrganizationApplicationResource,
|
||||||
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID),
|
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID),
|
||||||
data=dict(name="Some App", description="foo", application_uri="bar",
|
data=dict(name="Some App", description="foo", application_uri="bar",
|
||||||
redirect_uri="baz", gravatar_email="meh"))
|
redirect_uri="baz", avatar_email="meh"))
|
||||||
|
|
||||||
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, edit_json['client_id'])
|
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, edit_json['client_id'])
|
||||||
self.assertEquals("Some App", edit_json['name'])
|
self.assertEquals("Some App", edit_json['name'])
|
||||||
self.assertEquals("foo", edit_json['description'])
|
self.assertEquals("foo", edit_json['description'])
|
||||||
self.assertEquals("bar", edit_json['application_uri'])
|
self.assertEquals("bar", edit_json['application_uri'])
|
||||||
self.assertEquals("baz", edit_json['redirect_uri'])
|
self.assertEquals("baz", edit_json['redirect_uri'])
|
||||||
self.assertEquals("meh", edit_json['gravatar_email'])
|
self.assertEquals("meh", edit_json['avatar_email'])
|
||||||
|
|
||||||
# Retrieve the application again.
|
# Retrieve the application again.
|
||||||
json = self.getJsonResponse(OrganizationApplicationResource,
|
json = self.getJsonResponse(OrganizationApplicationResource,
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import hashlib
|
|
||||||
|
|
||||||
|
|
||||||
def compute_hash(email_address):
|
|
||||||
return hashlib.md5(email_address.strip().lower()).hexdigest()
|
|
|
@ -3,6 +3,7 @@ Defines utility methods for working with gzip streams.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import zlib
|
import zlib
|
||||||
|
import time
|
||||||
|
|
||||||
# Window size for decompressing GZIP streams.
|
# Window size for decompressing GZIP streams.
|
||||||
# This results in ZLIB automatically detecting the GZIP headers.
|
# This results in ZLIB automatically detecting the GZIP headers.
|
||||||
|
@ -35,4 +36,8 @@ def calculate_size_handler():
|
||||||
size_info.uncompressed_size += len(decompressor.decompress(current_data, CHUNK_SIZE))
|
size_info.uncompressed_size += len(decompressor.decompress(current_data, CHUNK_SIZE))
|
||||||
current_data = decompressor.unconsumed_tail
|
current_data = decompressor.unconsumed_tail
|
||||||
|
|
||||||
|
# Make sure we allow the scheduler to do other work if we get stuck in this tight loop.
|
||||||
|
if len(current_data) > 0:
|
||||||
|
time.sleep(0)
|
||||||
|
|
||||||
return size_info, fn
|
return size_info, fn
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from app import get_app_url
|
from app import get_app_url, avatar
|
||||||
from data import model
|
from data import model
|
||||||
from util.gravatar import compute_hash
|
|
||||||
from util.names import parse_robot_username
|
from util.names import parse_robot_username
|
||||||
from jinja2 import Template, Environment, FileSystemLoader, contextfilter
|
from jinja2 import Template, Environment, FileSystemLoader, contextfilter
|
||||||
|
|
||||||
|
@ -25,10 +24,10 @@ def user_reference(username):
|
||||||
alt = 'Organization' if user.organization else 'User'
|
alt = 'Organization' if user.organization else 'User'
|
||||||
return """
|
return """
|
||||||
<span>
|
<span>
|
||||||
<img src="http://www.gravatar.com/avatar/%s?s=16&d=identicon"
|
<img src="%s"
|
||||||
style="vertical-align: middle; margin-left: 6px; margin-right: 4px;" alt="%s">
|
style="vertical-align: middle; margin-left: 6px; margin-right: 4px;" alt="%s">
|
||||||
<b>%s</b>
|
<b>%s</b>
|
||||||
</span>""" % (compute_hash(user.email), alt, username)
|
</span>""" % (avatar.get_url(user.email, 16), alt, username)
|
||||||
|
|
||||||
|
|
||||||
def repository_tag_reference(repository_path_and_tag):
|
def repository_tag_reference(repository_path_and_tag):
|
||||||
|
@ -55,10 +54,10 @@ def repository_reference(pair):
|
||||||
|
|
||||||
return """
|
return """
|
||||||
<span style="white-space: nowrap;">
|
<span style="white-space: nowrap;">
|
||||||
<img src="http://www.gravatar.com/avatar/%s?s=16&d=identicon" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;">
|
<img src="%s" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;">
|
||||||
<a href="%s/repository/%s/%s">%s/%s</a>
|
<a href="%s/repository/%s/%s">%s/%s</a>
|
||||||
</span>
|
</span>
|
||||||
""" % (compute_hash(owner.email), get_app_url(), namespace, repository, namespace, repository)
|
""" % (avatar.get_url(owner.email, 16), get_app_url(), namespace, repository, namespace, repository)
|
||||||
|
|
||||||
|
|
||||||
def admin_reference(username):
|
def admin_reference(username):
|
||||||
|
|
|
@ -52,6 +52,9 @@ class GithubOAuthConfig(OAuthConfig):
|
||||||
def _api_endpoint(self):
|
def _api_endpoint(self):
|
||||||
return self.config.get('API_ENDPOINT', self._get_url(self._endpoint(), '/api/v3/'))
|
return self.config.get('API_ENDPOINT', self._get_url(self._endpoint(), '/api/v3/'))
|
||||||
|
|
||||||
|
def api_endpoint(self):
|
||||||
|
return self._api_endpoint()[0:-1]
|
||||||
|
|
||||||
def user_endpoint(self):
|
def user_endpoint(self):
|
||||||
api_endpoint = self._api_endpoint()
|
api_endpoint = self._api_endpoint()
|
||||||
return self._get_url(api_endpoint, 'user')
|
return self._get_url(api_endpoint, 'user')
|
||||||
|
|
|
@ -5,7 +5,6 @@ from flask.ext.mail import Message
|
||||||
|
|
||||||
from app import mail, app, get_app_url
|
from app import mail, app, get_app_url
|
||||||
from data import model
|
from data import model
|
||||||
from util.gravatar import compute_hash
|
|
||||||
from util.jinjautil import get_template_env
|
from util.jinjautil import get_template_env
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
@ -11,6 +11,7 @@ from threading import Thread
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from data.model import db
|
from data.model import db
|
||||||
|
from data.queue import WorkQueue
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ class Worker(object):
|
||||||
def extend_processing(self, seconds_from_now):
|
def extend_processing(self, seconds_from_now):
|
||||||
with self._current_item_lock:
|
with self._current_item_lock:
|
||||||
if self.current_queue_item is not None:
|
if self.current_queue_item is not None:
|
||||||
self._queue.extend_processing(self.current_queue_item, seconds_from_now)
|
WorkQueue.extend_processing(self.current_queue_item, seconds_from_now)
|
||||||
|
|
||||||
def run_watchdog(self):
|
def run_watchdog(self):
|
||||||
logger.debug('Running watchdog.')
|
logger.debug('Running watchdog.')
|
||||||
|
|
Reference in a new issue