Merge pull request #22 from coreos-inc/git

git's a pretty cool guy
This commit is contained in:
Jimmy Zelinskie 2015-04-23 17:33:36 -04:00
commit 2a13eade80
35 changed files with 797 additions and 136 deletions

View file

@ -93,8 +93,6 @@ class BuildComponent(BaseComponent):
self._build_failure('Could not load build job information', irbe)
base_image_information = {}
buildpack_url = self.user_files.get_file_url(build_job.repo_build.resource_key,
requires_cors=False)
# Add the pull robot information, if any.
if build_job.pull_credentials:
@ -107,6 +105,7 @@ class BuildComponent(BaseComponent):
# Parse the build queue item into build arguments.
# build_package: URL to the build package to download and untar/unzip.
# defaults to empty string to avoid requiring a pointer on the builder.
# 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').
@ -119,16 +118,30 @@ class BuildComponent(BaseComponent):
# 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.registry_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
'build_package': self.user_files.get_file_url(build_job.repo_build.resource_key,
requires_cors=False)
if build_job.repo_build.resource_key is not None else "",
'sub_directory': build_config.get('build_subdir', ''),
'repository': repository_name,
'registry': self.registry_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,
}
# If the trigger has a private key, it's using git, thus we should add
# git data to the build args.
# url: url used to clone the git repository
# sha: the sha1 identifier of the commit to check out
# private_key: the key used to get read access to the git repository
if build_job.repo_build.trigger.private_key is not None:
build_arguments['git'] = {
'url': build_config['trigger_metadata'].get('git_url', ''),
'sha': build_config['trigger_metadata'].get('commit_sha', ''),
'private_key': build_job.repo_build.trigger.private_key,
}
# Invoke the build.
logger.debug('Invoking build: %s', self.builder_realm)
logger.debug('With Arguments: %s', build_arguments)

View file

@ -389,7 +389,8 @@ class RepositoryBuildTrigger(BaseModel):
service = ForeignKeyField(BuildTriggerService, index=True)
repository = ForeignKeyField(Repository, index=True)
connected_user = QuayUserField()
auth_token = CharField()
auth_token = CharField(null=True)
private_key = TextField(null=True)
config = TextField(default='{}')
write_token = ForeignKeyField(AccessToken, null=True)
pull_robot = QuayUserField(allows_robots=True, null=True, related_name='triggerpullrobot')
@ -535,7 +536,7 @@ class RepositoryBuild(BaseModel):
uuid = CharField(default=uuid_generator, index=True)
repository = ForeignKeyField(Repository, index=True)
access_token = ForeignKeyField(AccessToken)
resource_key = CharField(index=True)
resource_key = CharField(index=True, null=True)
job_config = TextField()
phase = CharField(default=BUILD_PHASE.WAITING)
started = DateTimeField(default=datetime.now)

View file

@ -2,7 +2,7 @@ set -e
DOCKER_IP=`echo $DOCKER_HOST | sed 's/tcp:\/\///' | sed 's/:.*//'`
MYSQL_CONFIG_OVERRIDE="{\"DB_URI\":\"mysql+pymysql://root:password@$DOCKER_IP/genschema\"}"
PERCONA_CONFIG_OVERRIDE="{\"DB_URI\":\"mysql+pymysql://root@$DOCKER_IP/genschema\"}"
PERCONA_CONFIG_OVERRIDE="{\"DB_URI\":\"mysql+pymysql://root:password@$DOCKER_IP/genschema\"}"
PGSQL_CONFIG_OVERRIDE="{\"DB_URI\":\"postgresql://postgres@$DOCKER_IP/genschema\"}"
up_mysql() {
@ -41,14 +41,14 @@ down_mariadb() {
up_percona() {
# Run a SQL database on port 3306 inside of Docker.
docker run --name percona -p 3306:3306 -d percona
docker run --name percona -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password -d percona
# Sleep for 10s
echo 'Sleeping for 10...'
sleep 10
# Add the daabase to mysql.
docker run --rm --link percona:percona percona sh -c 'echo "create database genschema" | mysql -h $PERCONA_PORT_3306_TCP_ADDR'
docker run --rm --link percona:percona percona sh -c 'echo "create database genschema" | mysql -h $PERCONA_PORT_3306_TCP_ADDR -uroot -ppassword'
}
down_percona() {

View file

@ -0,0 +1,26 @@
"""add private key to build triggers
Revision ID: 214350b6a8b1
Revises: 2b2529fd23ff
Create Date: 2015-03-19 14:23:52.604505
"""
# revision identifiers, used by Alembic.
revision = '214350b6a8b1'
down_revision = '67eb43c778b'
from alembic import op
import sqlalchemy as sa
def upgrade(tables):
### commands auto generated by Alembic - please adjust! ###
op.add_column('repositorybuildtrigger', sa.Column('private_key', sa.Text(), nullable=True))
### end Alembic commands ###
def downgrade(tables):
### commands auto generated by Alembic - please adjust! ###
op.drop_column('repositorybuildtrigger', 'private_key')
### end Alembic commands ###

View file

@ -0,0 +1,30 @@
"""make resource_key nullable
Revision ID: 31288f79df53
Revises: 214350b6a8b1
Create Date: 2015-03-23 14:34:04.816295
"""
# revision identifiers, used by Alembic.
revision = '31288f79df53'
down_revision = '214350b6a8b1'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def upgrade(tables):
### commands auto generated by Alembic - please adjust! ###
op.alter_column('repositorybuild', 'resource_key',
existing_type=mysql.VARCHAR(length=255),
nullable=True)
### end Alembic commands ###
def downgrade(tables):
### commands auto generated by Alembic - please adjust! ###
op.alter_column('repositorybuild', 'resource_key',
existing_type=mysql.VARCHAR(length=255),
nullable=False)
### end Alembic commands ###

View file

@ -0,0 +1,30 @@
"""make auth_token nullable
Revision ID: 3fee6f979c2a
Revises: 31288f79df53
Create Date: 2015-03-27 11:11:24.046996
"""
# revision identifiers, used by Alembic.
revision = '3fee6f979c2a'
down_revision = '31288f79df53'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def upgrade(tables):
### commands auto generated by Alembic - please adjust! ###
op.alter_column('repositorybuildtrigger', 'auth_token',
existing_type=mysql.VARCHAR(length=255),
nullable=True)
### end Alembic commands ###
def downgrade(tables):
### commands auto generated by Alembic - please adjust! ###
op.alter_column('repositorybuildtrigger', 'auth_token',
existing_type=mysql.VARCHAR(length=255),
nullable=False)
### end Alembic commands ###

View file

@ -43,14 +43,13 @@ def user_view(user):
'is_robot': user.robot,
}
def trigger_view(trigger):
def trigger_view(trigger, can_admin=False):
if trigger and trigger.uuid:
config_dict = get_trigger_config(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
return {
'service': trigger.service.name,
'config': config_dict,
'config': config_dict if can_admin else {},
'id': trigger.uuid,
'connected_user': trigger.connected_user.username,
'is_active': build_trigger.is_active(config_dict),
@ -60,7 +59,7 @@ def trigger_view(trigger):
return None
def build_status_view(build_obj, can_write=False):
def build_status_view(build_obj, can_write=False, can_admin=False):
phase = build_obj.phase
try:
status = build_logs.get_status(build_obj.uuid)
@ -92,7 +91,7 @@ def build_status_view(build_obj, can_write=False):
'status': status or {},
'job_config': get_job_config(build_obj) if can_write else None,
'is_writer': can_write,
'trigger': trigger_view(build_obj.trigger),
'trigger': trigger_view(build_obj.trigger, can_admin),
'resource_key': build_obj.resource_key,
'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None,
'repository': {
@ -101,7 +100,7 @@ def build_status_view(build_obj, can_write=False):
}
}
if can_write:
if can_write and build_obj.resource_key is not None:
resp['archive_url'] = user_files.get_file_url(build_obj.resource_key, requires_cors=True)
return resp
@ -208,7 +207,7 @@ class RepositoryBuildList(RepositoryParamResource):
build_request = start_build(repo, dockerfile_id, tags, display_name, subdir, True,
pull_robot_name=pull_robot_name)
resp = build_status_view(build_request, True)
resp = build_status_view(build_request, can_write=True)
repo_string = '%s/%s' % (namespace, repository)
headers = {
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,

View file

@ -41,7 +41,7 @@ class BuildTriggerList(RepositoryParamResource):
""" List the triggers for the specified repository. """
triggers = model.list_build_triggers(namespace, repository)
return {
'triggers': [trigger_view(trigger) for trigger in triggers]
'triggers': [trigger_view(trigger, can_admin=True) for trigger in triggers]
}
@ -60,7 +60,7 @@ class BuildTrigger(RepositoryParamResource):
except model.InvalidBuildTriggerException:
raise NotFound()
return trigger_view(trigger)
return trigger_view(trigger, can_admin=True)
@require_repo_admin
@nickname('deleteBuildTrigger')
@ -207,24 +207,25 @@ class BuildTriggerActivate(RepositoryParamResource):
# Update the config.
new_config_dict = request.get_json()['config']
token_name = 'Build Trigger: %s' % trigger.service.name
token = model.create_delegate_token(namespace, repository, token_name,
'write')
write_token_name = 'Build Trigger: %s' % trigger.service.name
write_token = model.create_delegate_token(namespace, repository, write_token_name,
'write')
try:
path = url_for('webhooks.build_trigger_webhook', trigger_uuid=trigger.uuid)
authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', token.code,
authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'],
'$token', write_token.code,
app.config['SERVER_HOSTNAME'], path)
final_config = handler.activate(trigger.uuid, authed_url,
final_config, trigger.private_key = handler.activate(trigger.uuid, authed_url,
trigger.auth_token, new_config_dict)
except TriggerActivationException as exc:
token.delete_instance()
write_token.delete_instance()
raise request_error(message=exc.message)
# Save the updated config.
trigger.config = json.dumps(final_config)
trigger.write_token = token
trigger.write_token = write_token
trigger.save()
# Log the trigger setup.
@ -235,7 +236,7 @@ class BuildTriggerActivate(RepositoryParamResource):
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
'config': final_config}, repo=repo)
return trigger_view(trigger)
return trigger_view(trigger, can_admin=True)
else:
raise Unauthorized()
@ -373,6 +374,10 @@ class BuildTriggerAnalyze(RepositoryParamResource):
'status': 'error',
'message': rre.message
}
except NotImplementedError:
return {
'status': 'notimplemented',
}
raise NotFound()
@ -392,6 +397,10 @@ class ActivateBuildTrigger(RepositoryParamResource):
'branch_name': {
'type': 'string',
'description': '(GitHub Only) If specified, the name of the GitHub branch to build.'
},
'commit_sha': {
'type': 'string',
'description': '(Custom Only) If specified, the ref/SHA1 used to checkout a git repository.'
}
}
}
@ -414,7 +423,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
try:
run_parameters = request.get_json()
specs = handler.manual_start(trigger.auth_token, config_dict, run_parameters=run_parameters)
specs = handler.manual_start(trigger, run_parameters=run_parameters)
dockerfile_id, tags, name, subdir, metadata = specs
repo = model.get_repository(namespace, repository)
@ -426,7 +435,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
except TriggerStartException as tse:
raise InvalidRequest(tse.message)
resp = build_status_view(build_request, True)
resp = build_status_view(build_request, can_write=True)
repo_string = '%s/%s' % (namespace, repository)
headers = {
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,
@ -450,7 +459,7 @@ class TriggerBuildList(RepositoryParamResource):
builds = list(model.list_trigger_builds(namespace, repository,
trigger_uuid, limit))
return {
'builds': [build_status_view(build, True) for build in builds]
'builds': [build_status_view(build, can_write=True) for build in builds]
}

View file

@ -295,7 +295,7 @@ def attach_github_build_trigger(namespace, repository):
trigger.uuid)
logger.debug('Redirecting to full url: %s' % full_url)
logger.debug('Redirecting to full url: %s', full_url)
return redirect(full_url)
abort(403)

View file

@ -4,12 +4,15 @@ import os.path
import tarfile
import base64
import re
import json
from github import Github, UnknownObjectException, GithubException
from tempfile import SpooledTemporaryFile
from jsonschema import validate
from app import app, userfiles as user_files, github_trigger
from util.tarfileappender import TarfileAppender
from util.ssh import generate_ssh_keypair
client = app.config['HTTPCLIENT']
@ -25,6 +28,8 @@ CHUNK_SIZE = 512 * 1024
def should_skip_commit(message):
return '[skip build]' in message or '[build skip]' in message
class InvalidPayloadException(Exception):
pass
class BuildArchiveException(Exception):
pass
@ -60,16 +65,16 @@ class BuildTrigger(object):
def dockerfile_url(self, auth_token, config):
"""
Returns the URL at which the Dockerfile for the trigger can be found or None if none/not applicable.
Returns the URL at which the Dockerfile for the trigger is found or None if none/not applicable.
"""
return None
raise NotImplementedError
def load_dockerfile_contents(self, auth_token, config):
"""
Loads the Dockerfile found for the trigger's config and returns them or None if none could
be found/loaded.
"""
return None
raise NotImplementedError
def list_build_sources(self, auth_token):
"""
@ -85,7 +90,7 @@ class BuildTrigger(object):
"""
raise NotImplementedError
def handle_trigger_request(self, request, auth_token, config):
def handle_trigger_request(self, request, trigger):
"""
Transform the incoming request data into a set of actions. Returns a tuple
of usefiles resource id, docker tags, build name, and resource subdir.
@ -114,7 +119,7 @@ class BuildTrigger(object):
"""
raise NotImplementedError
def manual_start(self, auth_token, config, run_parameters = None):
def manual_start(self, trigger, run_parameters=None):
"""
Manually creates a repository build for this trigger.
"""
@ -146,8 +151,17 @@ class BuildTrigger(object):
def raise_unsupported():
raise io.UnsupportedOperation
def get_trigger_config(trigger):
try:
return json.loads(trigger.config)
except:
return {}
class GithubBuildTrigger(BuildTrigger):
"""
BuildTrigger for GitHub that uses the archive API and buildpacks.
"""
@staticmethod
def _get_client(auth_token):
return Github(auth_token,
@ -166,34 +180,70 @@ class GithubBuildTrigger(BuildTrigger):
new_build_source = config['build_source']
gh_client = self._get_client(auth_token)
# Find the GitHub repository.
try:
to_add_webhook = gh_client.get_repo(new_build_source)
gh_repo = gh_client.get_repo(new_build_source)
except UnknownObjectException:
msg = 'Unable to find GitHub repository for source: %s'
raise TriggerActivationException(msg % new_build_source)
msg = 'Unable to find GitHub repository for source: %s' % new_build_source
raise TriggerActivationException(msg)
# Add a deploy key to the GitHub repository.
public_key, private_key = generate_ssh_keypair()
config['credentials'] = [
{
'name': 'SSH Public Key',
'value': public_key,
},
]
try:
deploy_key = gh_repo.create_key('%s Builder' % app.config['REGISTRY_TITLE'],
public_key)
config['deploy_key_id'] = deploy_key.id
except GithubException:
msg = 'Unable to add deploy key to repository: %s' % new_build_source
raise TriggerActivationException(msg)
# Add the webhook to the GitHub repository.
webhook_config = {
'url': standard_webhook_url,
'content_type': 'json',
}
try:
hook = to_add_webhook.create_hook('web', webhook_config)
hook = gh_repo.create_hook('web', webhook_config)
config['hook_id'] = hook.id
config['master_branch'] = to_add_webhook.default_branch
config['master_branch'] = gh_repo.default_branch
except GithubException:
msg = 'Unable to create webhook on repository: %s'
raise TriggerActivationException(msg % new_build_source)
msg = 'Unable to create webhook on repository: %s' % new_build_source
raise TriggerActivationException(msg)
return config
return config, private_key
def deactivate(self, auth_token, config):
gh_client = self._get_client(auth_token)
# Find the GitHub repository.
try:
repo = gh_client.get_repo(config['build_source'])
to_delete = repo.get_hook(config['hook_id'])
to_delete.delete()
except UnknownObjectException:
msg = 'Unable to find GitHub repository for source: %s' % config['build_source']
raise TriggerDeactivationException(msg)
# If the trigger uses a deploy key, remove it.
try:
if config['deploy_key_id']:
deploy_key = repo.get_key(config['deploy_key_id'])
deploy_key.delete()
except KeyError:
# There was no config['deploy_key_id'], thus this is an old trigger without a deploy key.
pass
except GithubException:
msg = 'Unable to remove deploy key: %s' % config['deploy_key_id']
raise TriggerDeactivationException(msg)
# Remove the webhook.
try:
hook = repo.get_hook(config['hook_id'])
hook.delete()
except GithubException:
msg = 'Unable to remove hook: %s' % config['hook_id']
raise TriggerDeactivationException(msg)
@ -233,7 +283,8 @@ class GithubBuildTrigger(BuildTrigger):
return repos_by_org
def matches_ref(self, ref, regex):
@staticmethod
def matches_ref(ref, regex):
match_string = ref.split('/', 1)[1]
if not regex:
return False
@ -257,7 +308,7 @@ class GithubBuildTrigger(BuildTrigger):
try:
regex = re.compile(config['branchtag_regex'])
branches = [branch.name for branch in repo.get_branches()
if self.matches_ref('refs/heads/' + branch.name, regex)]
if GithubBuildTrigger.matches_ref('refs/heads/' + branch.name, regex)]
except:
pass
@ -275,17 +326,18 @@ class GithubBuildTrigger(BuildTrigger):
raise RepositoryReadException(message)
def dockerfile_url(self, auth_token, config):
def dockerfile_url(self, auth_token, config):
source = config['build_source']
subdirectory = config.get('subdir', '')
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
gh_client = self._get_client(auth_token)
try:
repo = gh_client.get_repo(source)
master_branch = repo.default_branch or 'master'
return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path)
except GithubException as ge:
except GithubException:
logger.exception('Could not load repository for Dockerfile.')
return None
def load_dockerfile_contents(self, auth_token, config):
@ -341,12 +393,12 @@ class GithubBuildTrigger(BuildTrigger):
return commit_info
@staticmethod
def _prepare_build(config, repo, commit_sha, build_name, ref):
def _prepare_tarball(repo, commit_sha):
# Prepare the download and upload URLs
archive_link = repo.get_archive_link('tarball', commit_sha)
download_archive = client.get(archive_link, stream=True)
tarball_subdir = ''
with SpooledTemporaryFile(CHUNK_SIZE) as tarball:
for chunk in download_archive.iter_content(CHUNK_SIZE):
tarball.write(chunk)
@ -372,6 +424,25 @@ class GithubBuildTrigger(BuildTrigger):
logger.debug('Successfully prepared job')
return tarball_subdir, dockerfile_id
@staticmethod
def _prepare_build(trigger, config, repo, commit_sha, build_name, ref, git_url):
repo_subdir = config['subdir']
joined_subdir = repo_subdir
dockerfile_id = None
if trigger.private_key is None:
# If the trigger isn't using git, prepare the buildpack.
tarball_subdir, dockerfile_id = GithubBuildTrigger._prepare_tarball(repo, commit_sha)
logger.debug('Successfully prepared job')
# Join provided subdir with the tarball subdir.
joined_subdir = os.path.join(tarball_subdir, repo_subdir)
logger.debug('Final subdir: %s', joined_subdir)
# compute the tag(s)
branch = ref.split('/')[-1]
tags = {branch}
@ -379,18 +450,14 @@ class GithubBuildTrigger(BuildTrigger):
if branch == repo.default_branch:
tags.add('latest')
logger.debug('Pushing to tags: %s' % tags)
# compute the subdir
repo_subdir = config['subdir']
joined_subdir = os.path.join(tarball_subdir, repo_subdir)
logger.debug('Final subdir: %s' % joined_subdir)
logger.debug('Pushing to tags: %s', tags)
# compute the metadata
metadata = {
'commit_sha': commit_sha,
'ref': ref,
'default_branch': repo.default_branch,
'git_url': git_url,
}
# add the commit info.
@ -404,7 +471,7 @@ class GithubBuildTrigger(BuildTrigger):
def get_display_name(sha):
return sha[0:7]
def handle_trigger_request(self, request, auth_token, config):
def handle_trigger_request(self, request, trigger):
payload = request.get_json()
if not payload or payload.get('head_commit') is None:
raise SkipRequestException()
@ -416,14 +483,16 @@ class GithubBuildTrigger(BuildTrigger):
ref = payload['ref']
commit_sha = payload['head_commit']['id']
commit_message = payload['head_commit'].get('message', '')
git_url = payload['repository']['git_url']
config = get_trigger_config(trigger)
if 'branchtag_regex' in config:
try:
regex = re.compile(config['branchtag_regex'])
except:
regex = re.compile('.*')
if not self.matches_ref(ref, regex):
if not GithubBuildTrigger.matches_ref(ref, regex):
raise SkipRequestException()
if should_skip_commit(commit_message):
@ -431,7 +500,7 @@ class GithubBuildTrigger(BuildTrigger):
short_sha = GithubBuildTrigger.get_display_name(commit_sha)
gh_client = self._get_client(auth_token)
gh_client = self._get_client(trigger.auth_token)
repo_full_name = '%s/%s' % (payload['repository']['owner']['name'],
payload['repository']['name'])
@ -439,24 +508,25 @@ class GithubBuildTrigger(BuildTrigger):
logger.debug('Github repo: %s', repo)
return GithubBuildTrigger._prepare_build(config, repo, commit_sha,
short_sha, ref)
return GithubBuildTrigger._prepare_build(trigger, config, repo, commit_sha,
short_sha, ref, git_url)
def manual_start(self, auth_token, config, run_parameters = None):
def manual_start(self, trigger, run_parameters=None):
config = get_trigger_config(trigger)
try:
source = config['build_source']
run_parameters = run_parameters or {}
gh_client = self._get_client(auth_token)
gh_client = self._get_client(trigger.auth_token)
repo = gh_client.get_repo(source)
branch_name = run_parameters.get('branch_name') or repo.default_branch
branch = repo.get_branch(branch_name)
branch_sha = branch.commit.sha
commit_info = branch.commit
short_sha = GithubBuildTrigger.get_display_name(branch_sha)
ref = 'refs/heads/%s' % (branch_name)
git_url = repo.git_url
return self._prepare_build(config, repo, branch_sha, short_sha, ref)
return self._prepare_build(trigger, config, repo, branch_sha, short_sha, ref, git_url)
except GithubException as ghe:
raise TriggerStartException(ghe.data['message'])
@ -491,3 +561,151 @@ class GithubBuildTrigger(BuildTrigger):
return branches
return None
class CustomBuildTrigger(BuildTrigger):
payload_schema = {
'type': 'object',
'properties': {
'commit': {
'type': 'string',
'description': 'first 7 characters of the SHA-1 identifier for a git commit',
'pattern': '^([A-Fa-f0-9]{7})$',
},
'ref': {
'type': 'string',
'description': 'git reference for a git commit',
'pattern': '^refs\/(heads|tags|remotes)\/(.+)$',
},
'default_branch': {
'type': 'string',
'description': 'default branch of the git repository',
},
'commit_info': {
'type': 'object',
'description': 'metadata about a git commit',
'properties': {
'url': {
'type': 'string',
'description': 'URL to view a git commit',
},
'message': {
'type': 'string',
'description': 'git commit message',
},
'date': {
'type': 'string',
'description': 'timestamp for a git commit'
},
'author': {
'type': 'object',
'description': 'metadata about the author of a git commit',
'properties': {
'username': {
'type': 'string',
'description': 'username of the author',
},
'url': {
'type': 'string',
'description': 'URL to view the profile of the author',
},
'avatar_url': {
'type': 'string',
'description': 'URL to view the avatar of the author',
},
},
'required': ['username', 'url', 'avatar_url'],
},
'committer': {
'type': 'object',
'description': 'metadata about the committer of a git commit',
'properties': {
'username': {
'type': 'string',
'description': 'username of the committer',
},
'url': {
'type': 'string',
'description': 'URL to view the profile of the committer',
},
'avatar_url': {
'type': 'string',
'description': 'URL to view the avatar of the committer',
},
},
'required': ['username', 'url', 'avatar_url'],
},
},
'required': ['url', 'message', 'date'],
},
},
'required': ['commits', 'ref', 'default_branch'],
}
@classmethod
def service_name(cls):
return 'custom-git'
def is_active(self, config):
return config.has_key('credentials')
def _metadata_from_payload(self, payload):
try:
metadata = json.loads(payload)
validate(metadata, self.payload_schema)
except:
raise InvalidPayloadException()
return metadata
def handle_trigger_request(self, request, trigger):
payload = request.get_json()
if not payload:
raise SkipRequestException()
logger.debug('Payload %s', payload)
metadata = self._metadata_from_payload(payload)
# The build source is the canonical git URL used to clone.
config = get_trigger_config(trigger)
metadata['git_url'] = config['build_source']
branch = metadata['ref'].split('/')[-1]
tags = {branch}
build_name = metadata['commit_sha'][:6]
dockerfile_id = None
return dockerfile_id, tags, build_name, trigger.config['subdir'], metadata
def activate(self, trigger_uuid, standard_webhook_url, auth_token, config):
public_key, private_key = generate_ssh_keypair()
config['credentials'] = [
{
'name': 'SSH Public Key',
'value': public_key,
},
{
'name': 'Webhook Endpoint URL',
'value': standard_webhook_url,
},
]
return config, private_key
def deactivate(self, auth_token, config):
config.pop('credentials', None)
return config
def manual_start(self, trigger, run_parameters=None):
# commit_sha is the only required parameter
if 'commit_sha' not in run_parameters:
raise TriggerStartException('missing required parameter')
config = get_trigger_config(trigger)
dockerfile_id = None
tags = {run_parameters['commit_sha']}
build_name = run_parameters['commit_sha']
metadata = {
'commit_sha': run_parameters['commit_sha'],
'git_url': config['build_source'],
}
return dockerfile_id, list(tags), build_name, config['subdir'], metadata

View file

@ -12,7 +12,7 @@ from data.model.oauth import DatabaseAuthorizationProvider
from app import app, billing as stripe, build_logs, avatar, signer
from auth.auth import require_session_login, process_oauth
from auth.permissions import (AdministerOrganizationPermission, ReadRepositoryPermission,
SuperUserPermission)
SuperUserPermission, AdministerRepositoryPermission)
from util.invoice import renderInvoiceToPdf
from util.seo import render_snapshot
@ -20,6 +20,7 @@ from util.cache import no_cache
from endpoints.common import common_login, render_page_template, route_show_if, param_required
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
from endpoints.registry import set_cache_headers
from endpoints.trigger import CustomBuildTrigger
from util.names import parse_repository_name, parse_repository_name_and_tag
from util.useremails import send_email_changed
from util.systemlogs import build_logs_archive
@ -494,6 +495,28 @@ def download_logs_archive():
abort(403)
@web.route('/customtrigger/setup/<path:repository>', methods=['GET'])
@require_session_login
@parse_repository_name
def attach_custom_build_trigger(namespace, repository_name):
permission = AdministerRepositoryPermission(namespace, repository_name)
if permission.can():
repo = model.get_repository(namespace, repository_name)
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository_name)
abort(404, message=msg)
trigger = model.create_build_trigger(repo, CustomBuildTrigger.service_name(),
None, current_user.db_user())
repo_path = '%s/%s' % (namespace, repository_name)
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
trigger.uuid)
logger.debug('Redirecting to full url: %s', full_url)
return redirect(full_url)
abort(403)
@web.route('/<path:repository>')
@no_cache

View file

@ -1,5 +1,4 @@
import logging
import json
from flask import request, make_response, Blueprint
@ -9,9 +8,8 @@ from auth.auth import process_auth
from auth.permissions import ModifyRepositoryPermission
from util.invoice import renderInvoiceToHtml
from util.useremails import send_invoice_email, send_subscription_change, send_payment_failed
from util.names import parse_repository_name
from util.http import abort
from endpoints.trigger import BuildTrigger, ValidationRequestException, SkipRequestException
from endpoints.trigger import BuildTrigger, ValidationRequestException, SkipRequestException, InvalidPayloadException
from endpoints.common import start_build
@ -23,7 +21,7 @@ webhooks = Blueprint('webhooks', __name__)
@webhooks.route('/stripe', methods=['POST'])
def stripe_webhook():
request_data = request.get_json()
logger.debug('Stripe webhook call: %s' % request_data)
logger.debug('Stripe webhook call: %s', request_data)
customer_id = request_data.get('data', {}).get('object', {}).get('customer', None)
user = model.get_user_or_org_by_customer_id(customer_id) if customer_id else None
@ -87,19 +85,18 @@ def build_trigger_webhook(trigger_uuid, **kwargs):
handler = BuildTrigger.get_trigger_for_service(trigger.service.name)
logger.debug('Passing webhook request to handler %s', handler)
config_dict = json.loads(trigger.config)
try:
specs = handler.handle_trigger_request(request, trigger.auth_token,
config_dict)
specs = handler.handle_trigger_request(request, trigger)
dockerfile_id, tags, name, subdir, metadata = specs
except ValidationRequestException:
# This was just a validation request, we don't need to build anything
return make_response('Okay')
except SkipRequestException:
# The build was requested to be skipped
return make_response('Okay')
except InvalidPayloadException:
# The payload was malformed
abort(400)
pull_robot_name = model.get_pull_robot_name(trigger)
repo = model.get_repository(namespace, repository)

View file

@ -203,6 +203,7 @@ def initialize_database():
LoginService.create(name='ldap')
BuildTriggerService.create(name='github')
BuildTriggerService.create(name='custom-git')
AccessTokenKind.create(name='build-worker')
AccessTokenKind.create(name='pushpull-token')

View file

@ -29,7 +29,7 @@
}
.build-view .build-icon-message.internalerror {
color: #DFFF00;
color: rgb(151, 168, 0);
}
.build-view .build-icon-message.complete {

View file

@ -81,6 +81,7 @@
</span>
<input type="url" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="url" required>
<input type="text" class="form-control" ng-model="currentConfig[field.name]" ng-switch-when="string" required>
<!-- TODO(jschorr): unify the ability to create an input box with all the usual features -->
<div ng-switch-when="regex">
<input type="text" class="form-control" ng-model="currentConfig[field.name]"
ng-pattern="getPattern(field)"

View file

@ -0,0 +1,24 @@
<div ng-switch on="trigger.service">
<!-- Message -->
<div ng-switch-when="custom-git" class="alert alert-info">
<p>
In order to use this trigger, the following first requires action:
<ul>
<li>You must give the following public key read access to the git repository.</li>
<li>You must set your repository to POST to the following URL to trigger a build.</li>
</ul>
For more information, refer to the <a href="http://docs.quay.io/guides/custom-trigger.html" target="_blank">Custom Git Triggers documentation</a>.
</p>
</div>
<div ng-switch-when="github" class="alert alert-info">
<p>The following key has been automatically added to your GitHub repository.</p>
</div>
<!-- Credentials -->
<div ng-repeat="credential in trigger.config.credentials">
<p>
{{ credential.name }}:
<div class="copy-box" value="credential.value"></div>
</p>
</div>
</div>

View file

@ -18,11 +18,20 @@
<span ng-switch-when="option">
<span class="quay-spinner" ng-show="!fieldOptions[field.name]"></span>
<select ng-model="parameters[field.name]" ng-show="fieldOptions[field.name]"
ng-options="value for value in fieldOptions[field.name]"
ng-options="value for value in fieldOptions[field.name]"
required>
</select>
</span>
<input type="text" class="form-control" ng-model="parameters[field.name]" ng-switch-when="string" required>
<!-- TODO(jschorr): unify the ability to create an input box with all the usual features -->
<div ng-switch-when="regex">
<input type="text" class="form-control" ng-model="parameters[field.name]"
ng-pattern="getPattern(field)"
placeholder="{{ field.placeholder }}"
ng-name="field.name"
id="{{ field.name }}"
required>
</div>
</div>
</td>
</tr>

View file

@ -93,7 +93,7 @@
</button>
<ul class="dropdown-menu dropdown-menu-right pull-right">
<li ng-repeat="type in TriggerService.getTypes()">
<a href="{{ TriggerService.getRedirectUrl(type, repository.namespace, repository.name) }}">
<a href="{{ TriggerService.getRedirectUrl(type, repository.namespace, repository.name) }}" target="{{ TriggerService.getMetadata(type).is_external ? '' : '_self' }}">
<i class="fa fa-lg" ng-class="TriggerService.getMetadata(type).icon"></i>
{{ TriggerService.getTitle(type) }}
</a>
@ -135,13 +135,16 @@
<tr ng-repeat="trigger in triggers | filter:{'is_active':true}">
<td><div class="trigger-description" trigger="trigger" short="true"></div></td>
<td>{{ trigger.subdir || '(Root Directory)' }}</td>
<td>{{ trigger.config.branchtag_regex || '(All)' }}</td>
<td>{{ trigger.config.subdir || '/' }}</td>
<td>{{ trigger.config.branchtag_regex || 'All' }}</td>
<td>
<span class="entity-reference" entity="trigger.pull_robot" ng-if="trigger.pull_robot"></span>
</td>
<td>
<span class="cor-options-menu">
<span ng-if="trigger.config.credentials" class="cor-option" option-click="showTriggerCredentialsModal(trigger)">
<i class="fa fa-unlock-alt"></i> View Credentials
</span>
<span class="cor-option" option-click="askRunTrigger(trigger)"
ng-class="trigger.connected_user == user.username ? '' : 'disabled'">
<i class="fa fa-chevron-right"></i> Run Trigger Now
@ -159,6 +162,11 @@
</div>
</div> <!-- /Build Triggers -->
<!-- Dialogs -->
<!-- Trigger Credentials dialog -->
<div class="trigger-credentials-dialog" trigger="triggerCredentialsModalTrigger" counter="triggerCredentialsModalCounter"></div>
<!-- Delete Tag Confirm -->
<div class="cor-confirm-dialog"
dialog-context="deleteTriggerInfo"
@ -190,4 +198,6 @@
counter="showTriggerStartDialogCounter"
start-build="startTrigger(trigger, parameters)"></div>
</div>
<!-- /Dialogs -->
</div>

View file

@ -19,6 +19,11 @@
next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
analyze="checkAnalyze(isValid)"></div>
</div>
<div ng-switch-when="custom-git">
<div class="trigger-setup-custom" repository="repository" trigger="trigger"
next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
analyze="checkAnalyze(isValid)"></div>
</div>
</div>
<!-- Loading pull information -->
@ -30,11 +35,13 @@
<div class="trigger-option-section" ng-show="currentView == 'analyzed'">
<!-- Messaging -->
<div class="alert alert-danger" ng-if="pullInfo.analysis.status == 'error'">
{{ pullInfo.analysis.message }}
</div>
<div class="alert alert-warning" ng-if="pullInfo.analysis.status == 'warning'">
{{ pullRequirements.message }}
<div ng-switch on="pullInfo.analysis.status">
<div ng-switch-when="error" class="alert alert-danger">{{ pullInfo.analysis.message }}</div>
<div ng-switch-when="warning" class="alert alert-warning">{{ pullInfo.analysis.message }}</div>
<div ng-switch-when="notimplemented" class="alert alert-warning">
<p>For {{ TriggerService.getTitle(trigger.service) }} triggers, we are unable to determine dependencies automatically.</p>
<p>If the git repository being built depends on a private base image, you must manually select a robot account with the proper permissions.</p>
</div>
</div>
<div class="dockerfile-found" ng-if="pullInfo.analysis.is_public === false">
<div class="dockerfile-found-content">
@ -54,7 +61,9 @@
</div>
</div>
<div style="margin-bottom: 12px">Please select the credentials to use when pulling the base image:</div>
<div style="margin-bottom: 12px">
Please select the credentials to use when pulling the base image:
</div>
<div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
<strong>Note:</strong> In order to set pull credentials for a build trigger, you must be an
Administrator of the namespace <strong>{{ repository.namespace }}</strong>
@ -101,6 +110,13 @@
</div>
<div class="trigger-option-section" ng-show="currentView == 'postActivation'">
<div ng-if="trigger.config.credentials" class="credentials" trigger="trigger"></div>
<div ng-if="!trigger.config.credentials">
<div class="alert alert-success">The trigger has been successfully created.</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-disabled="!state.stepValid"
@ -112,7 +128,7 @@
ng-click="activate()"
ng-show="currentView == 'analyzed'">Create Trigger</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ currentView == 'postActivation' ? 'Done' : 'Cancel' }}</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->

View file

@ -0,0 +1,19 @@
<!-- Modal message dialog -->
<div class="modal fade" id="triggercredentialsmodal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">
Trigger Credentials
</h4>
</div>
<div class="modal-body">
<div class="credentials" trigger="trigger"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Done</button>
</div>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /.modal -->

View file

@ -1,4 +1,5 @@
<span class="trigger-description-element" ng-switch on="trigger.service">
<!-- 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>
Push to GitHub <span ng-if="KeyService.isEnterprise('github-trigger')">Enterprise</span> repository
@ -14,11 +15,24 @@
<div>
<span class="trigger-description-subtitle">Dockerfile:</span>
<span ng-if="trigger.config.subdir">//{{ trigger.config.subdir}}/Dockerfile</span>
<span ng-if="!trigger.config.subdir">//Dockerfile</span>
<span>{{ TriggerService.getDockerfileLocation(trigger) }}</span>
</div>
</div>
</span>
<!-- Git -->
<span ng-switch-when="custom-git">
<i class="fa fa-git fa-lg" style="margin-right: 6px;" data-title="git" bs-tooltip="tooltip.title"></i>
Push to {{ trigger.config.build_source }}
<div style="margin-top: 4px; margin-left: 26px; font-size: 12px; color: gray;" ng-if="!short">
<div>
<span class="trigger-description-subtitle">Dockerfile:</span>
<span>{{ TriggerService.getDockerfileLocation(trigger) }}</span<
</div>
</div>
</span>
<!-- Who knows? -->
<span ng-switch-default>
Unknown
</span>

View file

@ -0,0 +1,40 @@
<div class="trigger-setup-custom-element">
<div class="selected-info" ng-show="nextStepCounter > 0">
<table style="width: 100%;">
<tr ng-show="nextStepCounter > 0">
<td width="200px">Repository</td>
<td>{{ state.build_source }}</td>
</tr>
<tr ng-show="nextStepCounter > 1">
<td>Dockerfile Location:</td>
<td>
<div class="dockerfile-location">
<i class="fa fa-folder fa-lg"></i> {{ state.subdir || '/' }}
</div>
</td>
</table>
</div>
<!-- Step view -->
<div class="step-view" next-step-counter="nextStepCounter" current-step-valid="currentStepValid"
steps-completed="stepsCompleted()">
<!-- Git URL Input -->
<!-- TODO(jschorr): make nopLoad(callback) no longer required -->
<div class="step-view-step" complete-condition="trigger['config']['build_source']" load-callback="nopLoad(callback)"
load-message="Loading Git URL Input">
<div style="margin-bottom: 12px;">Please enter an HTTP or SSH style URL used to clone your git repository:</div>
<input class="form-control" type="text" placeholder="git@example.com:namespace/repository.git" style="width: 100%;"
ng-model="state.build_source" ng-pattern="/(((http|https):\/\/)(.+)|\w+@(.+):(.+))/">
</div>
<!-- Dockerfile folder select -->
<div class="step-view-step" complete-condition="trigger.$ready" load-callback="nopLoad(callback)"
load-message="Loading Folder Input">
<div style="margin-bottom: 12px">Dockerfile Location:</div>
<input class="form-control" type="text" placeholder="/" style="width: 100%;"
ng-model="state.subdir" ng-pattern="/^($|\/|\/.+)/">
</div>
</div>
</div>

View file

@ -33,7 +33,7 @@
</td>
<td>
<div class="dockerfile-location">
<i class="fa fa-folder fa-lg"></i> {{ state.currentLocation || '(Repository Root)' }}
<i class="fa fa-folder fa-lg"></i> {{ state.currentLocation || '(Repository Root)' }}
</div>
</td>
</tr>

View file

@ -1,18 +1,17 @@
<div class="triggered-build-description-element">
<span class="tbd-content" class="manual" ng-if="!build.trigger && !build.job_config.manual_user">
(Manually Triggered Build)
</span>
<span class="tbd-content" class="manual" ng-if="!build.trigger && !build.job_config.manual_user">
(Manually Triggered Build)
</span>
<span class="tbd-content" ng-if="!build.trigger && build.job_config.manual_user">
<i class="fa fa-user"></i> {{ build.job_config.manual_user }}
</span>
<span class="tbd-content" ng-if="!build.trigger && build.job_config.manual_user">
<i class="fa fa-user"></i> {{ build.job_config.manual_user }}
</span>
<span ng-switch on="build.trigger.service" ng-if="build.trigger">
<!-- GitHub -->
<span ng-switch-when="github">
<!-- Full Commit Information -->
<span class="tbd-content" ng-if="build.job_config.trigger_metadata.commit_info">
<!-- Full Commit Information -->
<span class="tbd-content" ng-if="build.job_config.trigger_metadata.commit_info" ng-switch on="build.trigger.service">
<!-- GitHub -->
<div ng-switch-when="github">
<div class="commit-message">
<a ng-href="{{ getGitHubRepoURL(build) }}/commit/{{ build.job_config.trigger_metadata.commit_sha }}"
target="_blank">
@ -39,27 +38,63 @@
branch-template="getGitHubRepoURL(build) + '/tree/{branch}'"
tag-template="getGitHubRepoURL(build) + '/releases/tag/{tag}'"></span>
</div>
</span>
</div>
<!-- Just commit SHA -->
<span class="tbd-content" ng-if="build.job_config.trigger_metadata && !build.job_config.trigger_metadata.commit_info">
Triggered by commit
<!-- Git -->
<div ng-switch-when="custom-git">
<div class="commit-message">
{{ build.job_config.trigger_metadata.commit_info.message }}
</div>
<div class="commit-information">
<span class="commit-who-when">
Authored
<span am-time-ago="build.job_config.trigger_metadata.commit_info.date"></span>
<span class="commit-who">
{{ build.job_config.trigger_metadata.commit_info.author.username }}
</span>
</span>
<span>
{{ build.job_config.trigger_metadata.commit_sha }}
</span>
<span>
{{ build.job_config.trigger_metadata.ref }}
</span>
</div>
</div>
</span>
<!-- Just commit SHA -->
<span class="tbd-content" ng-if="build.job_config.trigger_metadata && !build.job_config.trigger_metadata.commit_info" ng-switch on="build.trigger.service">
Triggered by commit
<!-- GitHub -->
<div ng-switch-when="github">
<span class="source-commit-link"
commit-sha="build.job_config.trigger_metadata.commit_sha"
url-template="getGitHubRepoURL(build) + '/commit/{sha}'"></span>
</span>
</div>
<!-- No information -->
<span class="tbd-content" ng-if="!build.job_config.trigger_metadata">
Triggered by commit to
<!-- Git -->
<div ng-switch-when="custom-git">
<span>{{ build.job_config.trigger_metadata.commit_sha }}</span>
</div>
</span>
<!-- No information -->
<span class="tbd-content" ng-if="!build.job_config.trigger_metadata" ng-switch on="build.trigger.service">
Triggered by commit to
<!-- GitHub -->
<div ng-switch-when="github">
<i class="fa fa-github fa-lg" data-title="GitHub" data-container="body" bs-tooltip></i>
<a ng-href="{{ getGitHubRepoURL(build) }}" target="_new">
{{ build.trigger.config.build_source }}
</a>
</span>
</div>
<!-- Git -->
<div ng-switch-when="custom-git">
<i class="fa fa-git fa-lg" data-title="git" data-container="body" bs-tooltip></i>
{{ build.trigger.config.build_source }}
</div>
</span>
<!-- Unknown -->
<span ng-switch-default></span>
</span>
</div>

View file

@ -34,6 +34,9 @@ angular.module('quay').directive('repoPanelBuilds', function () {
$scope.showTriggerStartDialogCounter = 0;
$scope.showTriggerSetupCounter = 0;
$scope.triggerCredentialsModalTrigger = null;
$scope.triggerCredentialsModalCounter = 0;
var updateBuilds = function() {
if (!$scope.allBuilds) { return; }
@ -164,6 +167,11 @@ angular.module('quay').directive('repoPanelBuilds', function () {
$scope.options.predicate = predicate;
};
$scope.showTriggerCredentialsModal = function(trigger) {
$scope.triggerCredentialsModalTrigger = trigger;
$scope.triggerCredentialsModalCounter++;
};
$scope.askDeleteTrigger = function(trigger) {
$scope.deleteTriggerInfo = {
'trigger': trigger

View file

@ -0,0 +1,16 @@
/**
* An element which displays a credentials for a build trigger.
*/
angular.module('quay').directive('credentials', function() {
var directiveDefinitionObject = {
templateUrl: '/static/directives/credentials.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'trigger': '=trigger'
},
controller: function($scope) {}
};
return directiveDefinitionObject;
});

View file

@ -25,6 +25,10 @@ angular.module('quay').directive('manualTriggerBuildDialog', function () {
});
};
$scope.getPattern = function(field) {
return new RegExp(field.regex);
};
$scope.show = function() {
$scope.parameters = {};
$scope.fieldOptions = {};
@ -58,4 +62,4 @@ angular.module('quay').directive('manualTriggerBuildDialog', function () {
}
};
return directiveDefinitionObject;
});
});

View file

@ -14,12 +14,13 @@ angular.module('quay').directive('setupTriggerDialog', function () {
'canceled': '&canceled',
'activated': '&activated'
},
controller: function($scope, $element, ApiService, UserService) {
controller: function($scope, $element, ApiService, UserService, TriggerService) {
var modalSetup = false;
$scope.state = {};
$scope.nextStepCounter = -1;
$scope.currentView = 'config';
$scope.TriggerService = TriggerService
$scope.show = function() {
if (!$scope.trigger || !$scope.repository) { return; }
@ -113,10 +114,11 @@ angular.module('quay').directive('setupTriggerDialog', function () {
});
ApiService.activateBuildTrigger(data, params).then(function(resp) {
$scope.hide();
$scope.trigger['is_active'] = true;
$scope.trigger['config'] = resp['config'];
$scope.trigger['pull_robot'] = resp['pull_robot'];
$scope.activated({'trigger': $scope.trigger});
$scope.currentView = 'postActivation';
}, errorHandler);
};

View file

@ -0,0 +1,29 @@
/**
* An element which displays a dialog with the necessary credentials for a build trigger.
*/
angular.module('quay').directive('triggerCredentialsDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/trigger-credentials-dialog.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'trigger': '=trigger',
'counter': '=counter'
},
controller: function($scope, $element) {
var show = function() {
if (!$scope.trigger || !$scope.counter) {
$('#triggercredentialsmodal').modal('hide');
return;
}
$('#triggercredentialsmodal').modal({});
};
$scope.$watch('trigger', show);
$scope.$watch('counter', show);
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,49 @@
/**
* An element which displays custom git-specific setup information for its build triggers.
*/
angular.module('quay').directive('triggerSetupCustom', function() {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/trigger-setup-custom.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
'trigger': '=trigger',
'nextStepCounter': '=nextStepCounter',
'currentStepValid': '=currentStepValid',
'analyze': '&analyze'
},
controller: function($scope, $element, ApiService) {
$scope.analyzeCounter = 0;
$scope.setupReady = false;
$scope.state = {
'build_source': null,
'subdir': null
};
$scope.stepsCompleted = function() {
$scope.analyze({'isValid': $scope.state.build_source != null && $scope.state.subdir != null});
};
$scope.$watch('state.build_source', function(build_source) {
$scope.trigger['config']['build_source'] = build_source;
});
$scope.$watch('state.subdir', function(subdir) {
$scope.trigger['config']['subdir'] = subdir;
$scope.trigger.$ready = subdir != null;
});
$scope.nopLoad = function(callback) {
callback();
};
}
};
return directiveDefinitionObject;
});

View file

@ -69,7 +69,7 @@
$scope.isBuilding = function(build) {
if (!build) { return true; }
return build.phase != 'complete' && build.phase != 'error';
return build.phase != 'complete' && build.phase != 'error' && build.phase != 'internalerror';
};
}
})();
})();

View file

@ -64,4 +64,4 @@ angular.module('quay').factory('KeyService', ['$location', 'Config', function($l
};
return keyService;
}]);
}]);

View file

@ -2,8 +2,8 @@
* Helper service for defining the various kinds of build triggers and retrieving information
* about them.
*/
angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'KeyService', 'Features', 'CookieService',
function(UtilService, $sanitize, KeyService, Features, CookieService) {
angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'KeyService', 'Features', 'CookieService', 'Config',
function(UtilService, $sanitize, KeyService, Features, CookieService, Config) {
var triggerService = {};
var triggerTypes = {
@ -15,7 +15,6 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
desc += '<br>Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
return desc;
},
'run_parameters': [
{
'title': 'Branch',
@ -23,7 +22,6 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
'name': 'branch_name'
}
],
'get_redirect_url': function(namespace, repository) {
var redirect_uri = KeyService['githubRedirectUri'] + '/trigger/' +
namespace + '/' + repository;
@ -39,13 +37,11 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
return authorize_url + 'client_id=' + client_id +
'&scope=repo,user:email&redirect_uri=' + redirect_uri;
},
'is_external': true,
'is_enabled': function() {
return Features.GITHUB_BUILD;
},
'icon': 'fa-github',
'title': function() {
var isEnterprise = KeyService.isEnterprise('github-trigger');
if (isEnterprise) {
@ -54,6 +50,31 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
return 'GitHub Repository Push';
}
},
'custom-git': {
'description': function(config) {
var source = UtilService.textToSafeHtml(config['build_source']);
var desc = '<i class"fa fa-git fa-lg" style="margin-left:2px; margin-right: 2px"></i> Push to Custom Git Repository ' + source;
desc += '<br>Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
return desc;
},
'run_parameters': [
{
'title': 'Commit',
'type': 'regex',
'name': 'commit_sha',
'regex': '^([A-Fa-f0-9]{7})$',
'placeholder': '1c002dd'
}
],
'get_redirect_url': function(namespace, repository) {
return Config.getUrl('/customtrigger/setup/' + namespace + '/' + repository);
},
'is_external': false,
'is_enabled': function() { return true; },
'icon': 'fa-git',
'title': function() { return 'Custom Git Repository Push'; }
}
}
@ -76,6 +97,13 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
return type['get_redirect_url'](namespace, repository);
};
triggerService.getDockerfileLocation = function(trigger) {
if (!trigger.config.subdir) {
return '//Dockerfile';
}
return '//' + trigger.config.subdir.replace(new RegExp('(^\/+|\/+$)'), '') + '/Dockerfile';
};
triggerService.getTitle = function(name) {
var type = triggerTypes[name];
if (!type) {

Binary file not shown.

10
util/ssh.py Normal file
View file

@ -0,0 +1,10 @@
from Crypto.PublicKey import RSA
def generate_ssh_keypair():
"""
Generates a new 2048 bit RSA public key in OpenSSH format and private key in PEM format.
"""
key = RSA.generate(2048)
public_key = key.publickey().exportKey('OpenSSH')
private_key = key.exportKey('PEM')
return (public_key, private_key)