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
|
@ -124,12 +124,13 @@ class BuildComponent(BaseComponent):
|
||||||
# username: The username for pulling the base image (if any).
|
# username: The username for pulling the base image (if any).
|
||||||
# password: The password for pulling the base image (if any).
|
# password: The password for pulling the base image (if any).
|
||||||
|
|
||||||
subdir, dockerfile_name = self.name_and_path(build_config.get('build_subdir'))
|
# TODO: Charlie Tuesday, March 28, 2017 come back and clean up build_subdir.
|
||||||
|
dockerfile_path = os.path.relpath(build_config.get('build_subdir'), build_config.get('context'))
|
||||||
|
|
||||||
build_arguments = {
|
build_arguments = {
|
||||||
'build_package': build_job.get_build_package_url(self.user_files),
|
'build_package': build_job.get_build_package_url(self.user_files),
|
||||||
'sub_directory': subdir,
|
'context': build_config.get('context'),
|
||||||
'dockerfile_name': dockerfile_name,
|
'dockerfile_path': dockerfile_path,
|
||||||
'repository': repository_name,
|
'repository': repository_name,
|
||||||
'registry': self.registry_hostname,
|
'registry': self.registry_hostname,
|
||||||
'pull_token': build_job.repo_build.access_token.code,
|
'pull_token': build_job.repo_build.access_token.code,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from six import add_metaclass
|
from six import add_metaclass
|
||||||
|
@ -290,7 +291,7 @@ class BuildTriggerHandler(object):
|
||||||
def get_dockerfile_path(self):
|
def get_dockerfile_path(self):
|
||||||
""" Returns the normalized path to the Dockerfile found in the subdirectory
|
""" Returns the normalized path to the Dockerfile found in the subdirectory
|
||||||
in the config. """
|
in the config. """
|
||||||
dockerfile_path = self.config.get('subdir') or 'Dockerfile'
|
dockerfile_path = self.config.get('dockerfile_path') or 'Dockerfile'
|
||||||
if dockerfile_path[0] == '/':
|
if dockerfile_path[0] == '/':
|
||||||
dockerfile_path = dockerfile_path[1:]
|
dockerfile_path = dockerfile_path[1:]
|
||||||
return dockerfile_path
|
return dockerfile_path
|
||||||
|
@ -303,10 +304,11 @@ class BuildTriggerHandler(object):
|
||||||
ref = metadata.get('ref', None)
|
ref = metadata.get('ref', None)
|
||||||
commit_sha = metadata['commit']
|
commit_sha = metadata['commit']
|
||||||
default_branch = metadata.get('default_branch', None)
|
default_branch = metadata.get('default_branch', None)
|
||||||
|
|
||||||
prepared = PreparedBuild(self.trigger)
|
prepared = PreparedBuild(self.trigger)
|
||||||
prepared.name_from_sha(commit_sha)
|
prepared.name_from_sha(commit_sha)
|
||||||
prepared.subdirectory = config.get('subdir', None)
|
# TODO: Charlie Tuesday, March 28, 2017 come back and clean up subdirectory.
|
||||||
|
prepared.subdirectory = config.get('dockerfile_path', None)
|
||||||
|
prepared.context = config.get('context', None)
|
||||||
prepared.is_manual = is_manual
|
prepared.is_manual = is_manual
|
||||||
prepared.metadata = metadata
|
prepared.metadata = metadata
|
||||||
|
|
||||||
|
@ -327,3 +329,28 @@ class BuildTriggerHandler(object):
|
||||||
namespaces = list(namespaces_dict.values())
|
namespaces = list(namespaces_dict.values())
|
||||||
validate(namespaces, NAMESPACES_SCHEMA)
|
validate(namespaces, NAMESPACES_SCHEMA)
|
||||||
return namespaces
|
return namespaces
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_parent_directory_mappings(cls, dockerfile_path, current_paths=None):
|
||||||
|
""" Returns a map of dockerfile_paths to it's possible contexts. """
|
||||||
|
if dockerfile_path == "":
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if dockerfile_path[0] != os.path.sep:
|
||||||
|
dockerfile_path = os.path.sep + dockerfile_path
|
||||||
|
|
||||||
|
dockerfile_path = os.path.normpath(dockerfile_path)
|
||||||
|
all_paths = set()
|
||||||
|
path, _ = os.path.split(dockerfile_path)
|
||||||
|
if path == "":
|
||||||
|
path = os.path.sep
|
||||||
|
|
||||||
|
all_paths.add(path)
|
||||||
|
for i in range(1, len(path.split(os.path.sep))):
|
||||||
|
path, _ = os.path.split(path)
|
||||||
|
all_paths.add(path)
|
||||||
|
|
||||||
|
if current_paths:
|
||||||
|
return dict({dockerfile_path: list(all_paths)}, **current_paths)
|
||||||
|
|
||||||
|
return {dockerfile_path: list(all_paths)}
|
||||||
|
|
|
@ -371,17 +371,22 @@ class GithubBuildTrigger(BuildTriggerHandler):
|
||||||
raise RepositoryReadException(message)
|
raise RepositoryReadException(message)
|
||||||
|
|
||||||
path = self.get_dockerfile_path()
|
path = self.get_dockerfile_path()
|
||||||
if not path or not self.filename_is_dockerfile(os.path.basename(path)):
|
if not path:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_info = repo.get_file_contents(path)
|
file_info = repo.get_file_contents(path)
|
||||||
except GithubException as ghe:
|
# TypeError is needed because directory inputs cause a TypeError
|
||||||
|
except (GithubException, TypeError) as ghe:
|
||||||
|
logger.error("got error from trying to find github file %s" % ghe)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if file_info is None:
|
if file_info is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if isinstance(file_info, list):
|
||||||
|
return None
|
||||||
|
|
||||||
content = file_info.content
|
content = file_info.content
|
||||||
if file_info.encoding == 'base64':
|
if file_info.encoding == 'base64':
|
||||||
content = base64.b64decode(content)
|
content = base64.b64decode(content)
|
||||||
|
|
|
@ -4,11 +4,11 @@ from mock import Mock
|
||||||
from buildtrigger.bitbuckethandler import BitbucketBuildTrigger
|
from buildtrigger.bitbuckethandler import BitbucketBuildTrigger
|
||||||
from util.morecollections import AttrDict
|
from util.morecollections import AttrDict
|
||||||
|
|
||||||
def get_bitbucket_trigger(subdir=''):
|
def get_bitbucket_trigger(dockerfile_path=''):
|
||||||
trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger'))
|
trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger'))
|
||||||
trigger = BitbucketBuildTrigger(trigger_obj, {
|
trigger = BitbucketBuildTrigger(trigger_obj, {
|
||||||
'build_source': 'foo/bar',
|
'build_source': 'foo/bar',
|
||||||
'subdir': subdir,
|
'dockerfile_path': dockerfile_path,
|
||||||
'username': 'knownuser'
|
'username': 'knownuser'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@ from github import GithubException
|
||||||
from buildtrigger.githubhandler import GithubBuildTrigger
|
from buildtrigger.githubhandler import GithubBuildTrigger
|
||||||
from util.morecollections import AttrDict
|
from util.morecollections import AttrDict
|
||||||
|
|
||||||
def get_github_trigger(subdir=''):
|
def get_github_trigger(dockerfile_path=''):
|
||||||
trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger'))
|
trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger'))
|
||||||
trigger = GithubBuildTrigger(trigger_obj, {'build_source': 'foo', 'subdir': subdir})
|
trigger = GithubBuildTrigger(trigger_obj, {'build_source': 'foo', 'dockerfile_path': dockerfile_path})
|
||||||
trigger._get_client = get_mock_github
|
trigger._get_client = get_mock_github
|
||||||
return trigger
|
return trigger
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,11 @@ from mock import Mock
|
||||||
from buildtrigger.gitlabhandler import GitLabBuildTrigger
|
from buildtrigger.gitlabhandler import GitLabBuildTrigger
|
||||||
from util.morecollections import AttrDict
|
from util.morecollections import AttrDict
|
||||||
|
|
||||||
def get_gitlab_trigger(subdir=''):
|
def get_gitlab_trigger(dockerfile_path=''):
|
||||||
trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger'))
|
trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger'))
|
||||||
trigger = GitLabBuildTrigger(trigger_obj, {
|
trigger = GitLabBuildTrigger(trigger_obj, {
|
||||||
'build_source': 'foo/bar',
|
'build_source': 'foo/bar',
|
||||||
'subdir': subdir,
|
'dockerfile_path': dockerfile_path,
|
||||||
'username': 'knownuser'
|
'username': 'knownuser'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -13,3 +13,43 @@ from buildtrigger.basehandler import BuildTriggerHandler
|
||||||
])
|
])
|
||||||
def test_path_is_dockerfile(input, output):
|
def test_path_is_dockerfile(input, output):
|
||||||
assert BuildTriggerHandler.filename_is_dockerfile(input) == output
|
assert BuildTriggerHandler.filename_is_dockerfile(input) == output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('input,output', [
|
||||||
|
("", {}),
|
||||||
|
("/a", {"/a": ["/"]}),
|
||||||
|
("a", {"/a": ["/"]}),
|
||||||
|
("/b/a", {"/b/a": ["/b", "/"]}),
|
||||||
|
("b/a", {"/b/a": ["/b", "/"]}),
|
||||||
|
("/c/b/a", {"/c/b/a": ["/c/b", "/c", "/"]}),
|
||||||
|
("/a//b//c", {"/a/b/c": ["/", "/a", "/a/b"]}),
|
||||||
|
("/a", {"/a": ["/"]}),
|
||||||
|
])
|
||||||
|
def test_subdir_path_map_no_previous(input, output):
|
||||||
|
actual_mapping = BuildTriggerHandler.get_parent_directory_mappings(input)
|
||||||
|
for key in actual_mapping:
|
||||||
|
value = actual_mapping[key]
|
||||||
|
actual_mapping[key] = value.sort()
|
||||||
|
for key in output:
|
||||||
|
value = output[key]
|
||||||
|
output[key] = value.sort()
|
||||||
|
|
||||||
|
assert actual_mapping == output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('new_path,original_dictionary,output', [
|
||||||
|
("/a", {}, {"/a": ["/"]}),
|
||||||
|
("b", {"/a": ["some_path", "another_path"]}, {"/a": ["some_path", "another_path"], "/b": ["/"]}),
|
||||||
|
("/a/b/c/d", {"/e": ["some_path", "another_path"]},
|
||||||
|
{"/e": ["some_path", "another_path"], "/a/b/c/d": ["/", "/a", "/a/b", "/a/b/c"]}),
|
||||||
|
])
|
||||||
|
def test_subdir_path_map(new_path, original_dictionary, output):
|
||||||
|
actual_mapping = BuildTriggerHandler.get_parent_directory_mappings(new_path, original_dictionary)
|
||||||
|
for key in actual_mapping:
|
||||||
|
value = actual_mapping[key]
|
||||||
|
actual_mapping[key] = value.sort()
|
||||||
|
for key in output:
|
||||||
|
value = output[key]
|
||||||
|
output[key] = value.sort()
|
||||||
|
|
||||||
|
assert actual_mapping == output
|
||||||
|
|
|
@ -16,13 +16,13 @@ def test_list_build_subdirs(bitbucket_trigger):
|
||||||
assert bitbucket_trigger.list_build_subdirs() == ["/Dockerfile"]
|
assert bitbucket_trigger.list_build_subdirs() == ["/Dockerfile"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('subdir, contents', [
|
@pytest.mark.parametrize('dockerfile_path, contents', [
|
||||||
('/Dockerfile', 'hello world'),
|
('/Dockerfile', 'hello world'),
|
||||||
('somesubdir/Dockerfile', 'hi universe'),
|
('somesubdir/Dockerfile', 'hi universe'),
|
||||||
('unknownpath', None),
|
('unknownpath', None),
|
||||||
])
|
])
|
||||||
def test_load_dockerfile_contents(subdir, contents):
|
def test_load_dockerfile_contents(dockerfile_path, contents):
|
||||||
trigger = get_bitbucket_trigger(subdir)
|
trigger = get_bitbucket_trigger(dockerfile_path)
|
||||||
assert trigger.load_dockerfile_contents() == contents
|
assert trigger.load_dockerfile_contents() == contents
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -68,13 +68,13 @@ def test_handle_trigger_request(github_trigger, payload, expected_error, expecte
|
||||||
assert isinstance(github_trigger.handle_trigger_request(request), PreparedBuild)
|
assert isinstance(github_trigger.handle_trigger_request(request), PreparedBuild)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('subdir, contents', [
|
@pytest.mark.parametrize('dockerfile_path, contents', [
|
||||||
('/Dockerfile', 'hello world'),
|
('/Dockerfile', 'hello world'),
|
||||||
('somesubdir/Dockerfile', 'hi universe'),
|
('somesubdir/Dockerfile', 'hi universe'),
|
||||||
('unknownpath', None),
|
('unknownpath', None),
|
||||||
])
|
])
|
||||||
def test_load_dockerfile_contents(subdir, contents):
|
def test_load_dockerfile_contents(dockerfile_path, contents):
|
||||||
trigger = get_github_trigger(subdir)
|
trigger = get_github_trigger(dockerfile_path)
|
||||||
assert trigger.load_dockerfile_contents() == contents
|
assert trigger.load_dockerfile_contents() == contents
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,13 @@ def test_list_build_subdirs(gitlab_trigger):
|
||||||
assert gitlab_trigger.list_build_subdirs() == ['/Dockerfile']
|
assert gitlab_trigger.list_build_subdirs() == ['/Dockerfile']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('subdir, contents', [
|
@pytest.mark.parametrize('dockerfile_path, contents', [
|
||||||
('/Dockerfile', 'hello world'),
|
('/Dockerfile', 'hello world'),
|
||||||
('somesubdir/Dockerfile', 'hi universe'),
|
('somesubdir/Dockerfile', 'hi universe'),
|
||||||
('unknownpath', None),
|
('unknownpath', None),
|
||||||
])
|
])
|
||||||
def test_load_dockerfile_contents(subdir, contents):
|
def test_load_dockerfile_contents(dockerfile_path, contents):
|
||||||
trigger = get_gitlab_trigger(subdir)
|
trigger = get_gitlab_trigger(dockerfile_path)
|
||||||
assert trigger.load_dockerfile_contents() == contents
|
assert trigger.load_dockerfile_contents() == contents
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
"""back fill build expand_config
|
||||||
|
|
||||||
|
Revision ID: a6c463dfb9fe
|
||||||
|
Revises: e2894a3a3c19
|
||||||
|
Create Date: 2017-03-17 10:00:19.739858
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from data.database import RepositoryBuildTrigger
|
||||||
|
|
||||||
|
revision = 'a6c463dfb9fe'
|
||||||
|
down_revision = 'e2894a3a3c19'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
repostioryBuildTriggers = RepositoryBuildTrigger.select()
|
||||||
|
for repositoryBuildTrigger in repostioryBuildTriggers:
|
||||||
|
config = json.loads(repositoryBuildTrigger.config)
|
||||||
|
repositoryBuildTrigger.config = json.dumps(get_config_expand(config))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
repostioryBuildTriggers = RepositoryBuildTrigger.select()
|
||||||
|
for repositoryBuildTrigger in repostioryBuildTriggers:
|
||||||
|
config = json.loads(repositoryBuildTrigger.config)
|
||||||
|
repositoryBuildTrigger.config = json.dumps(get_config_expand(config))
|
||||||
|
|
||||||
|
|
||||||
|
def create_context(current_subdir):
|
||||||
|
if current_subdir == "":
|
||||||
|
current_subdir = os.path.sep + current_subdir
|
||||||
|
|
||||||
|
if current_subdir[len(current_subdir) - 1] != os.path.sep:
|
||||||
|
current_subdir += os.path.sep
|
||||||
|
|
||||||
|
context, _ = os.path.split(current_subdir)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def create_dockerfile_path(current_subdir):
|
||||||
|
if current_subdir == "":
|
||||||
|
current_subdir = os.path.sep + current_subdir
|
||||||
|
|
||||||
|
if current_subdir[len(current_subdir) - 1] != os.path.sep:
|
||||||
|
current_subdir += os.path.sep
|
||||||
|
|
||||||
|
return current_subdir + "Dockerfile"
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_expand(config):
|
||||||
|
""" A function to transform old records into new records """
|
||||||
|
if not config:
|
||||||
|
return config
|
||||||
|
|
||||||
|
# skip records that have been updated
|
||||||
|
if "context" in config or "dockerfile_path" in config:
|
||||||
|
return config
|
||||||
|
|
||||||
|
config_expand = {}
|
||||||
|
if "subdir" in config:
|
||||||
|
config_expand = dict(config)
|
||||||
|
config_expand["context"] = create_context(config["subdir"])
|
||||||
|
config_expand["dockerfile_path"] = create_dockerfile_path(config["subdir"])
|
||||||
|
|
||||||
|
return config_expand
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_contract(config):
|
||||||
|
""" A function to delete context and dockerfile_path from config """
|
||||||
|
if not config:
|
||||||
|
return config
|
||||||
|
|
||||||
|
if "context" in config:
|
||||||
|
del config["context"]
|
||||||
|
|
||||||
|
if "dockerfile_path" in config:
|
||||||
|
del config["dockerfile_path"]
|
||||||
|
|
||||||
|
return config
|
|
@ -18,10 +18,14 @@ PHASES_NOT_ALLOWED_TO_CANCEL_FROM = (BUILD_PHASE.PUSHING, BUILD_PHASE.COMPLETE,
|
||||||
ARCHIVABLE_BUILD_PHASES = [BUILD_PHASE.COMPLETE, BUILD_PHASE.ERROR, BUILD_PHASE.CANCELLED]
|
ARCHIVABLE_BUILD_PHASES = [BUILD_PHASE.COMPLETE, BUILD_PHASE.ERROR, BUILD_PHASE.CANCELLED]
|
||||||
|
|
||||||
|
|
||||||
def update_build_trigger(trigger, config, auth_token=None):
|
def update_build_trigger(trigger, config, auth_token=None, write_token=None):
|
||||||
trigger.config = json.dumps(_get_config_expand(config or {}))
|
trigger.config = json.dumps(_get_config_expand(config or {}))
|
||||||
if auth_token is not None:
|
if auth_token is not None:
|
||||||
trigger.auth_token = auth_token
|
trigger.auth_token = auth_token
|
||||||
|
|
||||||
|
if write_token is not None:
|
||||||
|
trigger.write_token = write_token
|
||||||
|
|
||||||
trigger.save()
|
trigger.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,44 +1,9 @@
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def _get_config_expand(config):
|
def _get_config_expand(config):
|
||||||
""" Get config with both context and dockerfile_path written to it """
|
""" Get config with both context and dockerfile_path written to it """
|
||||||
if config and "subdir" in config and "context" not in config:
|
if not config:
|
||||||
config_expand = dict(config)
|
return {}
|
||||||
config_expand["context"] = _create_context(config["subdir"])
|
if 'context' in config:
|
||||||
config_expand["dockerfile_path"] = _create_dockerfile_path(config["subdir"])
|
config['subdir'] = config['context']
|
||||||
return config_expand
|
|
||||||
return config or {}
|
|
||||||
|
|
||||||
|
return config
|
||||||
def _create_context(current_subdir):
|
|
||||||
""" Create context from current subdir """
|
|
||||||
if current_subdir.endswith("Dockerfile"):
|
|
||||||
context, _ = os.path.split(current_subdir)
|
|
||||||
if context == "":
|
|
||||||
return os.path.sep
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
if current_subdir == "":
|
|
||||||
current_subdir = os.path.sep + current_subdir
|
|
||||||
|
|
||||||
if current_subdir[len(current_subdir) - 1] != os.path.sep:
|
|
||||||
current_subdir += os.path.sep
|
|
||||||
|
|
||||||
context, _ = os.path.split(current_subdir)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
def _create_dockerfile_path(current_subdir):
|
|
||||||
""" Create dockefile path from current subdir """
|
|
||||||
if current_subdir.endswith("Dockerfile"):
|
|
||||||
return current_subdir
|
|
||||||
|
|
||||||
if current_subdir == "":
|
|
||||||
current_subdir = os.path.sep + current_subdir
|
|
||||||
|
|
||||||
if current_subdir[len(current_subdir) - 1] != os.path.sep:
|
|
||||||
current_subdir += os.path.sep
|
|
||||||
|
|
||||||
return current_subdir + "Dockerfile"
|
|
||||||
|
|
|
@ -4,27 +4,11 @@ from data.model.helpers.build_helper import _get_config_expand
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('org_config,expected', [
|
@pytest.mark.parametrize('org_config,expected', [
|
||||||
# Empty config
|
|
||||||
(None, {}),
|
(None, {}),
|
||||||
|
|
||||||
# No subdir in config
|
|
||||||
({}, {}),
|
({}, {}),
|
||||||
|
({'some other key': 'some other value'}, {'some other key': 'some other value'}),
|
||||||
({"subdir": ""}, {"context": "/", "dockerfile_path": "/Dockerfile", "subdir": ""}),
|
({'context': 'some/context', 'dockerfile_path': 'some/context/with/Dockerfile'},
|
||||||
({"subdir": "/"}, {"context": "/", "dockerfile_path": "/Dockerfile", "subdir": "/"}),
|
{'context': 'some/context', 'dockerfile_path': 'some/context/with/Dockerfile', 'subdir': 'some/context'}),
|
||||||
({"subdir": "/Dockerfile"}, {"context": "/", "dockerfile_path": "/Dockerfile", "subdir": "/Dockerfile"}),
|
|
||||||
({"subdir": "a"}, {"context": "a", "dockerfile_path": "a/Dockerfile", "subdir": "a"}),
|
|
||||||
({"subdir": "Dockerfile"}, {"context": "/", "dockerfile_path": "Dockerfile", "subdir": "Dockerfile"}),
|
|
||||||
({"subdir": "server.Dockerfile"},
|
|
||||||
{"context": "/", "dockerfile_path": "server.Dockerfile", "subdir": "server.Dockerfile"}),
|
|
||||||
({"subdir": "/a/b/Dockerfile"},
|
|
||||||
{"context": "/a/b", "dockerfile_path": "/a/b/Dockerfile", "subdir": "/a/b/Dockerfile"}),
|
|
||||||
({"subdir": "/a/b/server.Dockerfile"},
|
|
||||||
{"context": "/a/b", "dockerfile_path": "/a/b/server.Dockerfile", "subdir": "/a/b/server.Dockerfile"}),
|
|
||||||
({"subdir": "a/b/Dockerfile"}, {"context": "a/b", "dockerfile_path": "a/b/Dockerfile", "subdir": "a/b/Dockerfile"}),
|
|
||||||
({"subdir": "a/b/server.Dockerfile"},
|
|
||||||
{"context": "a/b", "dockerfile_path": "a/b/server.Dockerfile", "subdir": "a/b/server.Dockerfile"}),
|
|
||||||
({"subdir": "/a/b/c", "context": "slime"}, {"context": "slime", "subdir": "/a/b/c"}),
|
|
||||||
])
|
])
|
||||||
def test_super_user_build_endpoints(org_config, expected):
|
def test_super_user_build_endpoints(org_config, expected):
|
||||||
assert _get_config_expand(org_config) == expected
|
assert _get_config_expand(org_config) == expected
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" Create, list, cancel and get status/logs of repository builds. """
|
""" Create, list, cancel and get status/logs of repository builds. """
|
||||||
|
import os
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -51,6 +51,7 @@ def user_view(user):
|
||||||
'is_robot': user.robot,
|
'is_robot': user.robot,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def trigger_view(trigger, can_read=False, can_admin=False, for_build=False):
|
def trigger_view(trigger, can_read=False, can_admin=False, for_build=False):
|
||||||
if trigger and trigger.uuid:
|
if trigger and trigger.uuid:
|
||||||
build_trigger = BuildTriggerHandler.get_handler(trigger)
|
build_trigger = BuildTriggerHandler.get_handler(trigger)
|
||||||
|
@ -133,6 +134,8 @@ def build_status_view(build_obj):
|
||||||
'display_name': build_obj.display_name,
|
'display_name': build_obj.display_name,
|
||||||
'status': status or {},
|
'status': status or {},
|
||||||
'subdirectory': job_config.get('build_subdir', ''),
|
'subdirectory': job_config.get('build_subdir', ''),
|
||||||
|
'dockerfile_path': job_config.get('build_subdir', ''),
|
||||||
|
'context': job_config.get('context', ''),
|
||||||
'tags': job_config.get('docker_tags', []),
|
'tags': job_config.get('docker_tags', []),
|
||||||
'manual_user': job_config.get('manual_user', None),
|
'manual_user': job_config.get('manual_user', None),
|
||||||
'is_writer': can_write,
|
'is_writer': can_write,
|
||||||
|
@ -244,6 +247,7 @@ class RepositoryBuildList(RepositoryParamResource):
|
||||||
raise InvalidRequest('Invalid Archive URL: Must be http or https')
|
raise InvalidRequest('Invalid Archive URL: Must be http or https')
|
||||||
|
|
||||||
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
|
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'])
|
tags = request_json.get('docker_tags', ['latest'])
|
||||||
pull_robot_name = request_json.get('pull_robot', None)
|
pull_robot_name = request_json.get('pull_robot', None)
|
||||||
|
|
||||||
|
@ -291,9 +295,9 @@ class RepositoryBuildList(RepositoryParamResource):
|
||||||
prepared.archive_url = archive_url
|
prepared.archive_url = archive_url
|
||||||
prepared.tags = tags
|
prepared.tags = tags
|
||||||
prepared.subdirectory = subdir
|
prepared.subdirectory = subdir
|
||||||
|
prepared.context = context
|
||||||
prepared.is_manual = True
|
prepared.is_manual = True
|
||||||
prepared.metadata = {}
|
prepared.metadata = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name)
|
build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name)
|
||||||
except MaximumBuildsQueuedException:
|
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 json
|
||||||
import logging
|
import logging
|
||||||
|
from os import path
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
from urlparse import urlunparse
|
from urlparse import urlunparse
|
||||||
|
|
||||||
from flask import request, url_for
|
from flask import request, url_for
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
|
from auth.permissions import (UserAdminPermission, AdministerOrganizationPermission,
|
||||||
|
ReadRepositoryPermission, AdministerRepositoryPermission)
|
||||||
from buildtrigger.basehandler import BuildTriggerHandler
|
from buildtrigger.basehandler import BuildTriggerHandler
|
||||||
from buildtrigger.triggerutil import (TriggerDeactivationException,
|
from buildtrigger.triggerutil import (TriggerDeactivationException,
|
||||||
TriggerActivationException, EmptyRepositoryException,
|
TriggerActivationException, EmptyRepositoryException,
|
||||||
RepositoryReadException, TriggerStartException)
|
RepositoryReadException, TriggerStartException)
|
||||||
|
from data import model
|
||||||
|
from data.model.build import update_build_trigger
|
||||||
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, path_param, abort,
|
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.exception import NotFound, Unauthorized, InvalidRequest
|
||||||
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus
|
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus
|
||||||
from endpoints.building import start_build, MaximumBuildsQueuedException
|
from endpoints.building import start_build, MaximumBuildsQueuedException
|
||||||
from data import model
|
from endpoints.exception import NotFound, Unauthorized, InvalidRequest
|
||||||
from auth.permissions import (UserAdminPermission, AdministerOrganizationPermission,
|
|
||||||
ReadRepositoryPermission, AdministerRepositoryPermission)
|
|
||||||
from util.names import parse_robot_username
|
|
||||||
from util.dockerfileparse import parse_dockerfile
|
from util.dockerfileparse import parse_dockerfile
|
||||||
|
from util.names import parse_robot_username
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -131,19 +132,25 @@ class BuildTriggerSubdirs(RepositoryParamResource):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subdirs = handler.list_build_subdirs()
|
subdirs = handler.list_build_subdirs()
|
||||||
|
context_map = {}
|
||||||
|
for file in subdirs:
|
||||||
|
context_map = handler.get_parent_directory_mappings(file, context_map)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'subdir': ['/' + subdir for subdir in subdirs],
|
'dockerfile_paths': ['/' + subdir for subdir in subdirs],
|
||||||
'status': 'success'
|
'contextMap': context_map,
|
||||||
|
'status': 'success',
|
||||||
}
|
}
|
||||||
except EmptyRepositoryException as exc:
|
except EmptyRepositoryException as exc:
|
||||||
return {
|
return {
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'subdir': []
|
'contextMap': {},
|
||||||
|
'dockerfile_paths': [],
|
||||||
}
|
}
|
||||||
except RepositoryReadException as exc:
|
except RepositoryReadException as exc:
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': exc.message
|
'message': exc.message,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
@ -235,9 +242,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
raise request_error(message=exc.message)
|
raise request_error(message=exc.message)
|
||||||
|
|
||||||
# Save the updated config.
|
# Save the updated config.
|
||||||
trigger.config = json.dumps(final_config)
|
update_build_trigger(trigger, final_config, write_token=write_token)
|
||||||
trigger.write_token = write_token
|
|
||||||
trigger.save()
|
|
||||||
|
|
||||||
# Log the trigger setup.
|
# Log the trigger setup.
|
||||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
|
@ -343,6 +348,15 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
'message': 'Could not parse the Dockerfile specified'
|
'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.
|
# Default to the current namespace.
|
||||||
base_namespace = namespace_name
|
base_namespace = namespace_name
|
||||||
base_repository = None
|
base_repository = None
|
||||||
|
@ -399,6 +413,28 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
raise NotFound()
|
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')
|
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/start')
|
||||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
@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.'
|
'description': '(Custom Only) If specified, the ref/SHA1 used to checkout a git repository.'
|
||||||
},
|
},
|
||||||
'refs': {
|
'refs': {
|
||||||
'type': ['object', 'null'],
|
'type': ['object', 'null'],
|
||||||
'description': '(SCM Only) If specified, the ref to build.'
|
'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')
|
@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
|
||||||
@disallow_for_app_repositories
|
@disallow_for_app_repositories
|
||||||
@parse_args()
|
@parse_args()
|
||||||
|
@ -483,10 +520,12 @@ class TriggerBuildList(RepositoryParamResource):
|
||||||
|
|
||||||
FIELD_VALUE_LIMIT = 30
|
FIELD_VALUE_LIMIT = 30
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
|
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
|
||||||
@internal_only
|
@internal_only
|
||||||
class BuildTriggerFieldValues(RepositoryParamResource):
|
class BuildTriggerFieldValues(RepositoryParamResource):
|
||||||
""" Custom verb to fetch a values list for a particular field name. """
|
""" Custom verb to fetch a values list for a particular field name. """
|
||||||
|
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@disallow_for_app_repositories
|
@disallow_for_app_repositories
|
||||||
@nickname('listTriggerFieldValues')
|
@nickname('listTriggerFieldValues')
|
||||||
|
@ -558,13 +597,13 @@ class BuildTriggerSources(RepositoryParamResource):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/namespaces')
|
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/namespaces')
|
||||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||||
@internal_only
|
@internal_only
|
||||||
class BuildTriggerSourceNamespaces(RepositoryParamResource):
|
class BuildTriggerSourceNamespaces(RepositoryParamResource):
|
||||||
""" Custom verb to fetch the list of namespaces (orgs, projects, etc) for the trigger config. """
|
""" Custom verb to fetch the list of namespaces (orgs, projects, etc) for the trigger config. """
|
||||||
|
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@disallow_for_app_repositories
|
@disallow_for_app_repositories
|
||||||
@nickname('listTriggerBuildSourceNamespaces')
|
@nickname('listTriggerBuildSourceNamespaces')
|
||||||
|
|
|
@ -54,6 +54,7 @@ def start_build(repository, prepared_build, pull_robot_name=None):
|
||||||
'docker_tags': prepared_build.tags,
|
'docker_tags': prepared_build.tags,
|
||||||
'registry': host,
|
'registry': host,
|
||||||
'build_subdir': prepared_build.subdirectory,
|
'build_subdir': prepared_build.subdirectory,
|
||||||
|
'context': prepared_build.context,
|
||||||
'trigger_metadata': prepared_build.metadata or {},
|
'trigger_metadata': prepared_build.metadata or {},
|
||||||
'is_manual': prepared_build.is_manual,
|
'is_manual': prepared_build.is_manual,
|
||||||
'manual_user': get_authenticated_user().username if get_authenticated_user() else None,
|
'manual_user': get_authenticated_user().username if get_authenticated_user() else None,
|
||||||
|
@ -122,6 +123,7 @@ class PreparedBuild(object):
|
||||||
self._tags = None
|
self._tags = None
|
||||||
self._build_name = None
|
self._build_name = None
|
||||||
self._subdirectory = None
|
self._subdirectory = None
|
||||||
|
self._context = None
|
||||||
self._metadata = None
|
self._metadata = None
|
||||||
self._trigger = trigger
|
self._trigger = trigger
|
||||||
self._is_manual = None
|
self._is_manual = None
|
||||||
|
@ -224,6 +226,20 @@ class PreparedBuild(object):
|
||||||
|
|
||||||
self._subdirectory = value
|
self._subdirectory = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context(self):
|
||||||
|
if self._context is None:
|
||||||
|
raise Exception('Missing property context')
|
||||||
|
|
||||||
|
return self._context
|
||||||
|
|
||||||
|
@context.setter
|
||||||
|
def context(self, value):
|
||||||
|
if self._context:
|
||||||
|
raise Exception('Property context already set')
|
||||||
|
|
||||||
|
self._context = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
if self._metadata is None:
|
if self._metadata is None:
|
||||||
|
|
|
@ -605,6 +605,8 @@ def populate_database(minimal=False, with_storage=False):
|
||||||
trigger.config = json.dumps({
|
trigger.config = json.dumps({
|
||||||
'build_source': 'jakedt/testconnect',
|
'build_source': 'jakedt/testconnect',
|
||||||
'subdir': '',
|
'subdir': '',
|
||||||
|
'dockerfile_path': 'Dockerfile',
|
||||||
|
'context': '/',
|
||||||
})
|
})
|
||||||
trigger.save()
|
trigger.save()
|
||||||
|
|
||||||
|
|
|
@ -119,6 +119,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<td>Trigger Name</td>
|
<td>Trigger Name</td>
|
||||||
<td>Dockerfile Location</td>
|
<td>Dockerfile Location</td>
|
||||||
|
<td>Context Location</td>
|
||||||
<td>Branches/Tags</td>
|
<td>Branches/Tags</td>
|
||||||
<td>Pull Robot</td>
|
<td>Pull Robot</td>
|
||||||
<td class="options-col"></td>
|
<td class="options-col"></td>
|
||||||
|
@ -133,7 +134,8 @@
|
||||||
|
|
||||||
<tr ng-repeat="trigger in triggers | filter:{'is_active':true}">
|
<tr ng-repeat="trigger in triggers | filter:{'is_active':true}">
|
||||||
<td><div class="trigger-description" trigger="trigger"></div></td>
|
<td><div class="trigger-description" trigger="trigger"></div></td>
|
||||||
<td>{{ trigger.config.subdir || '/' }}</td>
|
<td>{{ trigger.config.dockerfile_path || '/Dockerfile' }}</td>
|
||||||
|
<td>{{ trigger.config.context || '/' }}</td>
|
||||||
<td>{{ trigger.config.branchtag_regex || 'All' }}</td>
|
<td>{{ trigger.config.branchtag_regex || 'All' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="entity-reference" entity="trigger.pull_robot" ng-if="trigger.pull_robot"></span>
|
<span class="entity-reference" entity="trigger.pull_robot" ng-if="trigger.pull_robot"></span>
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<div class="context-path-select-element">
|
||||||
|
<div class="dropdown-select" placeholder="'Enter a docker context'"
|
||||||
|
selected-item="$ctrl.selectedContext"
|
||||||
|
lookahead-items="$ctrl.contexts"
|
||||||
|
handle-input="$ctrl.setContext(input)"
|
||||||
|
handle-item-selected="$ctrl.setSelectedContext(datum.value)"
|
||||||
|
allow-custom-input="true"
|
||||||
|
hide-dropdown="$ctrl.contexts.length <= 0">
|
||||||
|
<!-- Icons -->
|
||||||
|
<i class="dropdown-select-icon none-icon fa fa-folder-o fa-lg"
|
||||||
|
ng-show="$ctrl.isUnknownContext"></i>
|
||||||
|
<i class="dropdown-select-icon none-icon fa fa-folder fa-lg" style="color: black;"
|
||||||
|
ng-show="!$ctrl.isUnknownContext"></i>
|
||||||
|
<i class="dropdown-select-icon fa fa-folder fa-lg"></i>
|
||||||
|
|
||||||
|
<!-- Dropdown menu -->
|
||||||
|
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||||
|
<li ng-repeat="context in $ctrl.contexts">
|
||||||
|
<a ng-click="$ctrl.setSelectedContext(context)"
|
||||||
|
ng-if="context">
|
||||||
|
<i class="fa fa-folder fa-lg"></i> {{ context }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 10px">
|
||||||
|
<div class="co-alert co-alert-danger"
|
||||||
|
ng-show="!$ctrl.isValidContext && $ctrl.currentContext">
|
||||||
|
Path is an invalid context.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { ContextPathSelectComponent } from './context-path-select.component';
|
||||||
|
|
||||||
|
|
||||||
|
describe("ContextPathSelectComponent", () => {
|
||||||
|
var component: ContextPathSelectComponent;
|
||||||
|
var currentContext: string;
|
||||||
|
var isValidContext: boolean;
|
||||||
|
var contexts: string[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component = new ContextPathSelectComponent();
|
||||||
|
currentContext = '/';
|
||||||
|
isValidContext = false;
|
||||||
|
contexts = ['/'];
|
||||||
|
component.currentContext = currentContext;
|
||||||
|
component.isValidContext = isValidContext;
|
||||||
|
component.contexts = contexts;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("$onChanges", () => {
|
||||||
|
|
||||||
|
it("sets valid context flag to true if current context is valid", () => {
|
||||||
|
component.$onChanges({});
|
||||||
|
|
||||||
|
expect(component.isValidContext).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets valid context flag to false if current context is invalid", () => {
|
||||||
|
component.currentContext = "asdfdsf";
|
||||||
|
component.$onChanges({});
|
||||||
|
|
||||||
|
expect(component.isValidContext).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setcontext", () => {
|
||||||
|
var newContext: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
newContext = '/conf';
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets current context to given context", () => {
|
||||||
|
component.setContext(newContext);
|
||||||
|
|
||||||
|
expect(component.currentContext).toEqual(newContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets valid context flag to true if given context is valid", () => {
|
||||||
|
component.setContext(newContext);
|
||||||
|
|
||||||
|
expect(component.isValidContext).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets valid context flag to false if given context is invalid", () => {
|
||||||
|
component.setContext("asdfsadfs");
|
||||||
|
|
||||||
|
expect(component.isValidContext).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setCurrentcontext", () => {
|
||||||
|
var context: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
context = '/conf';
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets current context to given context", () => {
|
||||||
|
component.setSelectedContext(context);
|
||||||
|
|
||||||
|
expect(component.currentContext).toEqual(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets valid context flag to true if given context is valid", () => {
|
||||||
|
component.setSelectedContext(context);
|
||||||
|
|
||||||
|
expect(component.isValidContext).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets valid context flag to false if given context is invalid", () => {
|
||||||
|
component.setSelectedContext("a;lskjdf;ldsa");
|
||||||
|
|
||||||
|
expect(component.isValidContext).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Input, Component } from 'angular-ts-decorators';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that allows the user to select the location of the Context in their source code repository.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'contextPathSelect',
|
||||||
|
templateUrl: '/static/js/directives/ui/context-path-select/context-path-select.component.html'
|
||||||
|
})
|
||||||
|
export class ContextPathSelectComponent implements ng.IComponentController {
|
||||||
|
|
||||||
|
// FIXME: Use one-way data binding
|
||||||
|
@Input('=') public currentContext: string;
|
||||||
|
@Input('=') public isValidContext: boolean;
|
||||||
|
@Input('=') public contexts: string[];
|
||||||
|
private isUnknownContext: boolean = true;
|
||||||
|
private selectedContext: string | null = null;
|
||||||
|
|
||||||
|
public $onChanges(changes: ng.IOnChangesObject): void {
|
||||||
|
this.isValidContext = this.checkContext(this.currentContext, this.contexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setContext(context: string): void {
|
||||||
|
this.currentContext = context;
|
||||||
|
this.selectedContext = null;
|
||||||
|
this.isValidContext = this.checkContext(context, this.contexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSelectedContext(context: string): void {
|
||||||
|
this.currentContext = context;
|
||||||
|
this.selectedContext = context;
|
||||||
|
this.isValidContext = this.checkContext(context, this.contexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkContext(context: string = '', contexts: string[] = []): boolean {
|
||||||
|
this.isUnknownContext = false;
|
||||||
|
var isValidContext: boolean = false;
|
||||||
|
|
||||||
|
if (context.length > 0 && context[0] === '/') {
|
||||||
|
isValidContext = true;
|
||||||
|
this.isUnknownContext = true;
|
||||||
|
}
|
||||||
|
return isValidContext;
|
||||||
|
}
|
||||||
|
}
|
|
@ -257,9 +257,10 @@
|
||||||
|
|
||||||
<dockerfile-path-select
|
<dockerfile-path-select
|
||||||
current-path="$ctrl.local.dockerfilePath"
|
current-path="$ctrl.local.dockerfilePath"
|
||||||
paths="$ctrl.local.dockerfileLocations.subdir"
|
paths="$ctrl.local.dockerfileLocations.dockerfile_paths"
|
||||||
supports-full-listing="true"
|
supports-full-listing="true"
|
||||||
is-valid-path="$ctrl.local.hasValidDockerfilePath"></dockerfile-path-select>
|
is-valid-path="$ctrl.local.hasValidDockerfilePath">
|
||||||
|
</dockerfile-path-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-8 col-md-8 col-sm-12 main-col"
|
<div class="col-lg-8 col-md-8 col-sm-12 main-col"
|
||||||
|
@ -268,10 +269,44 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col">
|
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col">
|
||||||
<p>Please select the location containing the Dockerfile to be built.</p>
|
<p>Please select the location containing the Dockerfile to be built.</p>
|
||||||
<p>The build context will start at the location selected.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</linear-workflow-section><!-- /Section: Dockerfile Location -->
|
</linear-workflow-section><!-- /Section: Dockerfile Location -->
|
||||||
|
|
||||||
|
<!-- Section: Context Location -->
|
||||||
|
<linear-workflow-section class="row"
|
||||||
|
section-id="contextlocation"
|
||||||
|
section-title="Select Docker Context"
|
||||||
|
section-valid="$ctrl.local.hasValidContextLocation">
|
||||||
|
<div class="col-lg-7 col-md-7 col-sm-12 main-col"
|
||||||
|
ng-if="$ctrl.local.dockerfileLocations.status == 'error'">
|
||||||
|
<div class="co-alert co-alert-warning">
|
||||||
|
{{ $ctrl.local.dockerfileLocations.message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-if="$ctrl.local.dockerfileLocations.status == 'success'">
|
||||||
|
<h3>Select Context</h3>
|
||||||
|
<strong>
|
||||||
|
Please select the context for the docker build
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<context-path-select
|
||||||
|
current-context="$ctrl.local.dockerContext"
|
||||||
|
current-path="$ctrl.local.dockerfilePath"
|
||||||
|
contexts="$ctrl.local.contexts"
|
||||||
|
is-valid-context="$ctrl.local.hasValidContextLocation">
|
||||||
|
</context-path-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8 col-md-8 col-sm-12 main-col"
|
||||||
|
ng-if="!$ctrl.local.dockerfileLocations">
|
||||||
|
<span class="cor-loader-inline"></span> Retrieving Dockerfile locations
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col">
|
||||||
|
<p>Please select a docker context.</p>
|
||||||
|
</div>
|
||||||
|
</linear-workflow-section><!-- /Section: Context Location -->
|
||||||
|
|
||||||
<!-- Section: Robot Account -->
|
<!-- Section: Robot Account -->
|
||||||
<linear-workflow-section class="row"
|
<linear-workflow-section class="row"
|
||||||
section-id="verification"
|
section-id="verification"
|
||||||
|
|
|
@ -80,7 +80,14 @@ export class ManageTriggerGithostComponent implements ng.IComponentController {
|
||||||
|
|
||||||
this.$scope.$watch(() => this.local.dockerfilePath, (path) => {
|
this.$scope.$watch(() => this.local.dockerfilePath, (path) => {
|
||||||
if (path && this.local.selectedRepository) {
|
if (path && this.local.selectedRepository) {
|
||||||
this.checkDockerfilePath(this.local.selectedRepository, path);
|
this.setPossibleContexts(path);
|
||||||
|
this.checkDockerfilePath(this.local.selectedRepository, path, this.local.dockerContext);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$scope.$watch(() => this.local.dockerContext, (context) => {
|
||||||
|
if (context && this.local.selectedRepository) {
|
||||||
|
this.checkDockerfilePath(this.local.selectedRepository, this.local.dockerfilePath, context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -117,7 +124,8 @@ export class ManageTriggerGithostComponent implements ng.IComponentController {
|
||||||
public createTrigger(): void {
|
public createTrigger(): void {
|
||||||
var config: any = {
|
var config: any = {
|
||||||
build_source: this.local.selectedRepository.full_name,
|
build_source: this.local.selectedRepository.full_name,
|
||||||
subdir: this.local.dockerfilePath.substr(1) // Remove starting /
|
dockerfile_path: this.local.dockerfilePath,
|
||||||
|
context: this.local.dockerContext
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.local.triggerOptions.hasBranchTagFilter &&
|
if (this.local.triggerOptions.hasBranchTagFilter &&
|
||||||
|
@ -271,6 +279,7 @@ export class ManageTriggerGithostComponent implements ng.IComponentController {
|
||||||
|
|
||||||
private loadDockerfileLocations(repository: any): void {
|
private loadDockerfileLocations(repository: any): void {
|
||||||
this.local.dockerfilePath = null;
|
this.local.dockerfilePath = null;
|
||||||
|
this.local.dockerContext = null;
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'repository': this.repository.namespace + '/' + this.repository.name,
|
'repository': this.repository.namespace + '/' + this.repository.name,
|
||||||
|
@ -301,7 +310,7 @@ export class ManageTriggerGithostComponent implements ng.IComponentController {
|
||||||
[]);
|
[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkDockerfilePath(repository: any, path: string): void {
|
private checkDockerfilePath(repository: any, path: string, context: string): void {
|
||||||
this.local.triggerAnalysis = null;
|
this.local.triggerAnalysis = null;
|
||||||
this.local.robotAccount = null;
|
this.local.robotAccount = null;
|
||||||
|
|
||||||
|
@ -312,7 +321,8 @@ export class ManageTriggerGithostComponent implements ng.IComponentController {
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
'build_source': repository.full_name,
|
'build_source': repository.full_name,
|
||||||
'subdir': path.substr(1)
|
'dockerfile_path': path.substr(1),
|
||||||
|
'context': context
|
||||||
};
|
};
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
|
@ -325,6 +335,12 @@ export class ManageTriggerGithostComponent implements ng.IComponentController {
|
||||||
this.buildOrderedRobotAccounts();
|
this.buildOrderedRobotAccounts();
|
||||||
}, this.ApiService.errorDisplay('Could not analyze trigger'));
|
}, this.ApiService.errorDisplay('Could not analyze trigger'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setPossibleContexts(path){
|
||||||
|
if(this.local.dockerfileLocations.contextMap){
|
||||||
|
this.local.contexts = this.local.dockerfileLocations.contextMap[path] || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { RegexMatchViewComponent } from "./directives/ui/regex-match-view/regex-
|
||||||
import { NgModule } from "angular-ts-decorators";
|
import { NgModule } from "angular-ts-decorators";
|
||||||
import { QuayRoutes } from "./quay-routes.module";
|
import { QuayRoutes } from "./quay-routes.module";
|
||||||
import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-select/dockerfile-path-select.component';
|
import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-select/dockerfile-path-select.component';
|
||||||
|
import { ContextPathSelectComponent } from './directives/ui/context-path-select/context-path-select.component';
|
||||||
import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component';
|
import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component';
|
||||||
import { ManageTriggerGithostComponent } from './directives/ui/manage-trigger-githost/manage-trigger-githost.component';
|
import { ManageTriggerGithostComponent } from './directives/ui/manage-trigger-githost/manage-trigger-githost.component';
|
||||||
import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear-workflow.component';
|
import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear-workflow.component';
|
||||||
|
@ -31,6 +32,7 @@ import { DataFileServiceImpl } from './services/datafile/datafile.service.impl';
|
||||||
declarations: [
|
declarations: [
|
||||||
RegexMatchViewComponent,
|
RegexMatchViewComponent,
|
||||||
DockerfilePathSelectComponent,
|
DockerfilePathSelectComponent,
|
||||||
|
ContextPathSelectComponent,
|
||||||
ManageTriggerCustomGitComponent,
|
ManageTriggerCustomGitComponent,
|
||||||
ManageTriggerGithostComponent,
|
ManageTriggerGithostComponent,
|
||||||
LinearWorkflowComponent,
|
LinearWorkflowComponent,
|
||||||
|
|
Binary file not shown.
|
@ -3852,6 +3852,7 @@ class FakeBuildTrigger(BuildTriggerHandler):
|
||||||
prepared.subdirectory = 'subdir'
|
prepared.subdirectory = 'subdir'
|
||||||
prepared.metadata = {'foo': 'bar'}
|
prepared.metadata = {'foo': 'bar'}
|
||||||
prepared.is_manual = True
|
prepared.is_manual = True
|
||||||
|
prepared.context = '/'
|
||||||
return prepared
|
return prepared
|
||||||
|
|
||||||
def get_repository_url(self):
|
def get_repository_url(self):
|
||||||
|
@ -4046,7 +4047,11 @@ class TestBuildTriggers(ApiTestCase):
|
||||||
trigger_uuid=trigger.uuid),
|
trigger_uuid=trigger.uuid),
|
||||||
data={'somevalue': 'meh'})
|
data={'somevalue': 'meh'})
|
||||||
|
|
||||||
self.assertEquals({'status': 'success', 'subdir': ['/sometoken', '/foo', '/bar', '/meh']},
|
self.assertEquals({'status': 'success', 'dockerfile_paths': ['/sometoken', '/foo', '/bar', '/meh'],
|
||||||
|
'contextMap': {'/bar': ['/'],
|
||||||
|
'/foo': ['/'],
|
||||||
|
'/meh': ['/'],
|
||||||
|
'/sometoken': ['/']}},
|
||||||
subdir_json)
|
subdir_json)
|
||||||
|
|
||||||
# Activate the trigger.
|
# Activate the trigger.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import json
|
||||||
from app import app
|
from app import app
|
||||||
from data import model
|
from data import model
|
||||||
from data.database import RepositoryBuildTrigger, configure
|
from data.database import RepositoryBuildTrigger, configure
|
||||||
|
from data.model.build import update_build_trigger
|
||||||
|
|
||||||
configure(app.config)
|
configure(app.config)
|
||||||
|
|
||||||
|
@ -40,8 +41,7 @@ def run_branchregex_migration():
|
||||||
config['branchtag_regex'] = new_regex
|
config['branchtag_regex'] = new_regex
|
||||||
|
|
||||||
logger.debug("Updating to branchtag regex '%s'", new_regex)
|
logger.debug("Updating to branchtag regex '%s'", new_regex)
|
||||||
trigger.config = json.dumps(config)
|
update_build_trigger(trigger, config)
|
||||||
trigger.save()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
Reference in a new issue