feat(build runner): added in context, dockerfile_location
this is a new feature meant to allow people to use any file as a dockerfile and any folder as a context directory
This commit is contained in:
parent
90b130fe16
commit
e6d201e0b0
29 changed files with 531 additions and 111 deletions
|
@ -1,5 +1,5 @@
|
|||
""" Create, list, cancel and get status/logs of repository builds. """
|
||||
|
||||
import os
|
||||
from urlparse import urlparse
|
||||
|
||||
import logging
|
||||
|
@ -51,6 +51,7 @@ def user_view(user):
|
|||
'is_robot': user.robot,
|
||||
}
|
||||
|
||||
|
||||
def trigger_view(trigger, can_read=False, can_admin=False, for_build=False):
|
||||
if trigger and trigger.uuid:
|
||||
build_trigger = BuildTriggerHandler.get_handler(trigger)
|
||||
|
@ -133,6 +134,8 @@ def build_status_view(build_obj):
|
|||
'display_name': build_obj.display_name,
|
||||
'status': status or {},
|
||||
'subdirectory': job_config.get('build_subdir', ''),
|
||||
'dockerfile_path': job_config.get('build_subdir', ''),
|
||||
'context': job_config.get('context', ''),
|
||||
'tags': job_config.get('docker_tags', []),
|
||||
'manual_user': job_config.get('manual_user', None),
|
||||
'is_writer': can_write,
|
||||
|
@ -244,6 +247,7 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
raise InvalidRequest('Invalid Archive URL: Must be http or https')
|
||||
|
||||
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
|
||||
context = request_json['context'] if 'context' in request_json else os.path.dirname(subdir)
|
||||
tags = request_json.get('docker_tags', ['latest'])
|
||||
pull_robot_name = request_json.get('pull_robot', None)
|
||||
|
||||
|
@ -291,9 +295,9 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
prepared.archive_url = archive_url
|
||||
prepared.tags = tags
|
||||
prepared.subdirectory = subdir
|
||||
prepared.context = context
|
||||
prepared.is_manual = True
|
||||
prepared.metadata = {}
|
||||
|
||||
try:
|
||||
build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name)
|
||||
except MaximumBuildsQueuedException:
|
||||
|
|
22
endpoints/api/test/test_trigger.py
Normal file
22
endpoints/api/test/test_trigger.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
import pytest
|
||||
|
||||
from endpoints.api.trigger import is_parent
|
||||
|
||||
|
||||
@pytest.mark.parametrize('context,dockerfile_path,expected', [
|
||||
("/", "/a/b", True),
|
||||
("/a", "/a/b", True),
|
||||
("/a/b", "/a/b", False),
|
||||
("/a//", "/a/b", True),
|
||||
("/a", "/a//b/c", True),
|
||||
("/a//", "a/b", True),
|
||||
("/a/b", "a/bc/d", False),
|
||||
("/d", "/a/b", False),
|
||||
("/a/b", "/a/b.c", False),
|
||||
("/a/b", "/a/b/b.c", True),
|
||||
("", "/a/b.c", False),
|
||||
("/a/b", "", False),
|
||||
("", "", False),
|
||||
])
|
||||
def test_super_user_build_endpoints(context, dockerfile_path, expected):
|
||||
assert is_parent(context, dockerfile_path) == expected
|
|
@ -2,17 +2,21 @@
|
|||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from os import path
|
||||
from urllib import quote
|
||||
from urlparse import urlunparse
|
||||
|
||||
from flask import request, url_for
|
||||
|
||||
from app import app
|
||||
from auth.permissions import (UserAdminPermission, AdministerOrganizationPermission,
|
||||
ReadRepositoryPermission, AdministerRepositoryPermission)
|
||||
from buildtrigger.basehandler import BuildTriggerHandler
|
||||
from buildtrigger.triggerutil import (TriggerDeactivationException,
|
||||
TriggerActivationException, EmptyRepositoryException,
|
||||
RepositoryReadException, TriggerStartException)
|
||||
from data import model
|
||||
from data.model.build import update_build_trigger
|
||||
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
||||
log_action, request_error, query_param, parse_args, internal_only,
|
||||
validate_json_request, api, path_param, abort,
|
||||
|
@ -20,12 +24,9 @@ from endpoints.api import (RepositoryParamResource, nickname, resource, require_
|
|||
from endpoints.exception import NotFound, Unauthorized, InvalidRequest
|
||||
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus
|
||||
from endpoints.building import start_build, MaximumBuildsQueuedException
|
||||
from data import model
|
||||
from auth.permissions import (UserAdminPermission, AdministerOrganizationPermission,
|
||||
ReadRepositoryPermission, AdministerRepositoryPermission)
|
||||
from util.names import parse_robot_username
|
||||
from endpoints.exception import NotFound, Unauthorized, InvalidRequest
|
||||
from util.dockerfileparse import parse_dockerfile
|
||||
|
||||
from util.names import parse_robot_username
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -131,19 +132,25 @@ class BuildTriggerSubdirs(RepositoryParamResource):
|
|||
|
||||
try:
|
||||
subdirs = handler.list_build_subdirs()
|
||||
context_map = {}
|
||||
for file in subdirs:
|
||||
context_map = handler.get_parent_directory_mappings(file, context_map)
|
||||
|
||||
return {
|
||||
'subdir': ['/' + subdir for subdir in subdirs],
|
||||
'status': 'success'
|
||||
'dockerfile_paths': ['/' + subdir for subdir in subdirs],
|
||||
'contextMap': context_map,
|
||||
'status': 'success',
|
||||
}
|
||||
except EmptyRepositoryException as exc:
|
||||
return {
|
||||
'status': 'success',
|
||||
'subdir': []
|
||||
'contextMap': {},
|
||||
'dockerfile_paths': [],
|
||||
}
|
||||
except RepositoryReadException as exc:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': exc.message
|
||||
'message': exc.message,
|
||||
}
|
||||
else:
|
||||
raise Unauthorized()
|
||||
|
@ -235,9 +242,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
raise request_error(message=exc.message)
|
||||
|
||||
# Save the updated config.
|
||||
trigger.config = json.dumps(final_config)
|
||||
trigger.write_token = write_token
|
||||
trigger.save()
|
||||
update_build_trigger(trigger, final_config, write_token=write_token)
|
||||
|
||||
# Log the trigger setup.
|
||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||
|
@ -343,6 +348,15 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
|||
'message': 'Could not parse the Dockerfile specified'
|
||||
}
|
||||
|
||||
# Check whether the dockerfile_path is correct
|
||||
if new_config_dict.get('context'):
|
||||
if not is_parent(new_config_dict.get('context'), new_config_dict.get('dockerfile_path')):
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Dockerfile, %s, is not child of the context, %s.' %
|
||||
(new_config_dict.get('context'), new_config_dict.get('dockerfile_path'))
|
||||
}
|
||||
|
||||
# Default to the current namespace.
|
||||
base_namespace = namespace_name
|
||||
base_repository = None
|
||||
|
@ -399,6 +413,28 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
|||
raise NotFound()
|
||||
|
||||
|
||||
def is_parent(context, dockerfile_path):
|
||||
""" This checks whether the context is a parent of the dockerfile_path"""
|
||||
if context == "" or dockerfile_path == "":
|
||||
return False
|
||||
|
||||
normalized_context = path.normpath(context)
|
||||
if normalized_context[len(normalized_context) - 1] != path.sep:
|
||||
normalized_context += path.sep
|
||||
|
||||
if normalized_context[0] != path.sep:
|
||||
normalized_context = path.sep + normalized_context
|
||||
|
||||
normalized_subdir = path.normpath(path.dirname(dockerfile_path))
|
||||
if normalized_subdir[0] != path.sep:
|
||||
normalized_subdir = path.sep + normalized_subdir
|
||||
|
||||
if normalized_subdir[len(normalized_subdir) - 1] != path.sep:
|
||||
normalized_subdir += path.sep
|
||||
|
||||
return normalized_subdir.startswith(normalized_context)
|
||||
|
||||
|
||||
@resource('/v1/repository/<apirepopath: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')
|
||||
|
@ -418,7 +454,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
'description': '(Custom Only) If specified, the ref/SHA1 used to checkout a git repository.'
|
||||
},
|
||||
'refs': {
|
||||
'type': ['object', 'null'],
|
||||
'type': ['object', 'null'],
|
||||
'description': '(SCM Only) If specified, the ref to build.'
|
||||
}
|
||||
},
|
||||
|
@ -467,6 +503,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class TriggerBuildList(RepositoryParamResource):
|
||||
""" Resource to represent builds that were activated from the specified trigger. """
|
||||
|
||||
@require_repo_admin
|
||||
@disallow_for_app_repositories
|
||||
@parse_args()
|
||||
|
@ -483,10 +520,12 @@ class TriggerBuildList(RepositoryParamResource):
|
|||
|
||||
FIELD_VALUE_LIMIT = 30
|
||||
|
||||
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
|
||||
@internal_only
|
||||
class BuildTriggerFieldValues(RepositoryParamResource):
|
||||
""" Custom verb to fetch a values list for a particular field name. """
|
||||
|
||||
@require_repo_admin
|
||||
@disallow_for_app_repositories
|
||||
@nickname('listTriggerFieldValues')
|
||||
|
@ -558,13 +597,13 @@ class BuildTriggerSources(RepositoryParamResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/namespaces')
|
||||
@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
|
||||
class BuildTriggerSourceNamespaces(RepositoryParamResource):
|
||||
""" Custom verb to fetch the list of namespaces (orgs, projects, etc) for the trigger config. """
|
||||
|
||||
@require_repo_admin
|
||||
@disallow_for_app_repositories
|
||||
@nickname('listTriggerBuildSourceNamespaces')
|
||||
|
|
Reference in a new issue