Merge remote-tracking branch 'origin/master' into benjamins
This commit is contained in:
commit
4946dca804
13 changed files with 759 additions and 131 deletions
|
@ -16,8 +16,9 @@ from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactiva
|
|||
TriggerActivationException, EmptyRepositoryException,
|
||||
RepositoryReadException)
|
||||
from data import model
|
||||
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission
|
||||
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
|
||||
from util.names import parse_robot_username
|
||||
from util.dockerfileparse import parse_dockerfile
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -232,6 +233,141 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
|
||||
@internal_only
|
||||
class BuildTriggerAnalyze(RepositoryParamResource):
|
||||
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
|
||||
(such as a robot account to use for pulling)
|
||||
"""
|
||||
schemas = {
|
||||
'BuildTriggerAnalyzeRequest': {
|
||||
'id': 'BuildTriggerAnalyzeRequest',
|
||||
'type': 'object',
|
||||
'required': [
|
||||
'config'
|
||||
],
|
||||
'properties': {
|
||||
'config': {
|
||||
'type': 'object',
|
||||
'description': 'Arbitrary json.',
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@require_repo_admin
|
||||
@nickname('analyzeBuildTrigger')
|
||||
@validate_json_request('BuildTriggerAnalyzeRequest')
|
||||
def post(self, namespace, repository, trigger_uuid):
|
||||
""" Analyze the specified build trigger configuration. """
|
||||
try:
|
||||
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
|
||||
except model.InvalidBuildTriggerException:
|
||||
raise NotFound()
|
||||
|
||||
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
|
||||
new_config_dict = request.get_json()['config']
|
||||
|
||||
try:
|
||||
# Load the contents of the Dockerfile.
|
||||
contents = handler.load_dockerfile_contents(trigger.auth_token, new_config_dict)
|
||||
if not contents:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Could not read the Dockerfile for the trigger'
|
||||
}
|
||||
|
||||
# Parse the contents of the Dockerfile.
|
||||
parsed = parse_dockerfile(contents)
|
||||
if not parsed:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Could not parse the Dockerfile specified'
|
||||
}
|
||||
|
||||
# Determine the base image (i.e. the FROM) for the Dockerfile.
|
||||
base_image = parsed.get_base_image()
|
||||
if not base_image:
|
||||
return {
|
||||
'status': 'warning',
|
||||
'message': 'No FROM line found in the Dockerfile'
|
||||
}
|
||||
|
||||
# Check to see if the base image lives in Quay.
|
||||
quay_registry_prefix = '%s/' % (app.config['URL_HOST'])
|
||||
|
||||
if not base_image.startswith(quay_registry_prefix):
|
||||
return {
|
||||
'status': 'publicbase'
|
||||
}
|
||||
|
||||
# Lookup the repository in Quay.
|
||||
result = base_image[len(quay_registry_prefix):].split('/', 2)
|
||||
if len(result) != 2:
|
||||
return {
|
||||
'status': 'warning',
|
||||
'message': '"%s" is not a valid Quay repository path' % (base_image)
|
||||
}
|
||||
|
||||
(base_namespace, base_repository) = result
|
||||
found_repository = model.get_repository(base_namespace, base_repository)
|
||||
if not found_repository:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Repository "%s" was not found' % (base_image)
|
||||
}
|
||||
|
||||
# If the repository is private and the user cannot see that repo, then
|
||||
# mark it as not found.
|
||||
can_read = ReadRepositoryPermission(base_namespace, base_repository)
|
||||
if found_repository.visibility.name != 'public' and not can_read:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Repository "%s" was not found' % (base_image)
|
||||
}
|
||||
|
||||
# Check to see if the repository is public. If not, we suggest the
|
||||
# usage of a robot account to conduct the pull.
|
||||
read_robots = []
|
||||
|
||||
if AdministerOrganizationPermission(base_namespace).can():
|
||||
def robot_view(robot):
|
||||
return {
|
||||
'name': robot.username,
|
||||
'kind': 'user',
|
||||
'is_robot': True
|
||||
}
|
||||
|
||||
def is_valid_robot(user):
|
||||
# Make sure the user is a robot.
|
||||
if not user.robot:
|
||||
return False
|
||||
|
||||
# Make sure the current user can see/administer the robot.
|
||||
(robot_namespace, shortname) = parse_robot_username(user.username)
|
||||
return AdministerOrganizationPermission(robot_namespace).can()
|
||||
|
||||
repo_perms = model.get_all_repo_users(base_namespace, base_repository)
|
||||
read_robots = [robot_view(perm.user) for perm in repo_perms if is_valid_robot(perm.user)]
|
||||
|
||||
return {
|
||||
'namespace': base_namespace,
|
||||
'name': base_repository,
|
||||
'is_public': found_repository.visibility.name == 'public',
|
||||
'robots': read_robots,
|
||||
'status': 'analyzed',
|
||||
'dockerfile_url': handler.dockerfile_url(trigger.auth_token, new_config_dict)
|
||||
}
|
||||
|
||||
except RepositoryReadException as rre:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': rre.message
|
||||
}
|
||||
|
||||
raise NotFound()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
||||
class ActivateBuildTrigger(RepositoryParamResource):
|
||||
""" Custom verb to manually activate a build trigger. """
|
||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
|||
import io
|
||||
import os.path
|
||||
import tarfile
|
||||
import base64
|
||||
|
||||
from github import Github, UnknownObjectException, GithubException
|
||||
from tempfile import SpooledTemporaryFile
|
||||
|
@ -46,6 +47,19 @@ class BuildTrigger(object):
|
|||
def __init__(self):
|
||||
pass
|
||||
|
||||
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.
|
||||
"""
|
||||
return None
|
||||
|
||||
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
|
||||
|
||||
def list_build_sources(self, auth_token):
|
||||
"""
|
||||
Take the auth information for the specific trigger type and load the
|
||||
|
@ -168,7 +182,6 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
return config
|
||||
|
||||
|
||||
def list_build_sources(self, auth_token):
|
||||
gh_client = self._get_client(auth_token)
|
||||
usr = gh_client.get_user()
|
||||
|
@ -219,6 +232,41 @@ class GithubBuildTrigger(BuildTrigger):
|
|||
|
||||
raise RepositoryReadException(message)
|
||||
|
||||
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.master_branch or 'master'
|
||||
return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path)
|
||||
except GithubException as ge:
|
||||
return None
|
||||
|
||||
def load_dockerfile_contents(self, auth_token, config):
|
||||
gh_client = self._get_client(auth_token)
|
||||
|
||||
source = config['build_source']
|
||||
subdirectory = config.get('subdir', '')
|
||||
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
|
||||
|
||||
try:
|
||||
repo = gh_client.get_repo(source)
|
||||
file_info = repo.get_file_contents(path)
|
||||
if file_info is None:
|
||||
return None
|
||||
|
||||
content = file_info.content
|
||||
if file_info.encoding == 'base64':
|
||||
content = base64.b64decode(content)
|
||||
return content
|
||||
|
||||
except GithubException as ge:
|
||||
message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source)
|
||||
raise RepositoryReadException(message)
|
||||
|
||||
@staticmethod
|
||||
def _prepare_build(config, repo, commit_sha, build_name, ref):
|
||||
# Prepare the download and upload URLs
|
||||
|
|
|
@ -295,7 +295,7 @@ def populate_database():
|
|||
|
||||
__generate_repository(new_user_1, 'complex',
|
||||
'Complex repository with many branches and tags.',
|
||||
False, [(new_user_2, 'read')],
|
||||
False, [(new_user_2, 'read'), (dtrobot[0], 'read')],
|
||||
(2, [(3, [], 'v2.0'),
|
||||
(1, [(1, [(1, [], ['prod'])],
|
||||
'staging'),
|
||||
|
|
|
@ -3463,7 +3463,7 @@ pre.command:before {
|
|||
|
||||
position: relative;
|
||||
|
||||
height: 100px;
|
||||
height: 75px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
@ -3614,10 +3614,10 @@ pre.command:before {
|
|||
margin-right: 34px;
|
||||
}
|
||||
|
||||
.trigger-option-section:not(:last-child) {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
.trigger-option-section:not(:first-child) {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.trigger-option-section .entity-search-element .twitter-typeahead {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<span class="entity-search-element" ng-class="isPersistent ? 'persistent' : ''"><input class="entity-search-control form-control">
|
||||
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntity" entity="currentEntity"></span>
|
||||
<span class="entity-reference block-reference" ng-show="isPersistent && currentEntityInternal" entity="currentEntityInternal"></span>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" id="entityDropdownMenu" data-toggle="dropdown"
|
||||
ng-click="lazyLoad()">
|
||||
|
|
100
static/directives/setup-trigger-dialog.html
Normal file
100
static/directives/setup-trigger-dialog.html
Normal file
|
@ -0,0 +1,100 @@
|
|||
<div class="setup-trigger-directive-element">
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="setupTriggerModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Setup new build trigger</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Trigger-specific setup -->
|
||||
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
|
||||
<div ng-switch-when="github">
|
||||
<div class="trigger-setup-github" repository="repository" trigger="trigger"
|
||||
analyze="checkAnalyze(isValid)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pull information -->
|
||||
<div class="trigger-option-section" ng-show="showPullRequirements">
|
||||
<div ng-show="!pullRequirements">
|
||||
<span class="quay-spinner"></span> Checking pull credential requirements...
|
||||
</div>
|
||||
|
||||
<div ng-show="pullRequirements">
|
||||
<div class="alert alert-danger" ng-if="pullRequirements.status == 'error'">
|
||||
{{ pullRequirements.message }}
|
||||
</div>
|
||||
<div class="alert alert-warning" ng-if="pullRequirements.status == 'warning'">
|
||||
{{ pullRequirements.message }}
|
||||
</div>
|
||||
<div class="alert alert-success" ng-if="pullRequirements.status == 'analyzed' && pullRequirements.is_public === false">
|
||||
The
|
||||
<a href="{{ pullRequirements.dockerfile_url }}" ng-if="pullRequirements.dockerfile_url" target="_blank">Dockerfile found</a>
|
||||
<span ng-if="!pullRequirements.dockerfile_url">Dockerfile found</span>
|
||||
depends on repository
|
||||
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}" target="_blank">
|
||||
{{ pullRequirements.namespace }}/{{ pullRequirements.name }}
|
||||
</a> which requires
|
||||
a robot account for pull access, because it is marked <strong>private</strong>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table style="width: 100%;" ng-show="pullRequirements">
|
||||
<tr>
|
||||
<td style="width: 114px">
|
||||
<div class="context-tooltip" data-title="The credentials used by the builder when pulling images" bs-tooltip>
|
||||
Pull Credentials:
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
|
||||
In order to set pull credentials for a build trigger, you must be an Administrator of the namespace <strong>{{ repository.namespace }}</strong>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" ng-if="isNamespaceAdmin(repository.namespace)">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="publicPull ? 'active btn-info' : ''" ng-click="setPublicPull(true)">Public</button>
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="publicPull ? '' : 'active btn-info'" ng-click="setPublicPull(false)">
|
||||
<i class="fa fa-wrench"></i>
|
||||
Robot account
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="!publicPull">
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div class="entity-search" namespace="repository.namespace" include-teams="false"
|
||||
input-title="'Select robot account for pulling...'"
|
||||
is-organization="repository.is_organization"
|
||||
is-persistent="true"
|
||||
current-entity="pullEntity"
|
||||
filter="['robot']"></div>
|
||||
|
||||
<div class="alert alert-info" ng-if="pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
||||
Note: We've automatically selected robot account <span class="entity-reference" entity="pullRequirements.robots[0]"></span>, since it has access to the repository.
|
||||
</div>
|
||||
<div class="alert alert-warning" ng-if="!pullRequirements.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
||||
Note: No robot account currently has access to the repository. Please create one and/or assign access in the
|
||||
<a href="/repository/{{ pullRequirements.namespace }}/{{ pullRequirements.name }}/admin" target="_blank">repository's admin panel</a>.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-disabled="!trigger.$ready || (!publicPull && !pullEntity) || checkingPullRequirements"
|
||||
ng-click="activate">Finished</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
171
static/js/app.js
171
static/js/app.js
|
@ -2681,6 +2681,7 @@ quayApp.directive('entitySearch', function () {
|
|||
controller: function($scope, $element, Restangular, UserService, ApiService) {
|
||||
$scope.lazyLoading = true;
|
||||
$scope.isAdmin = false;
|
||||
$scope.currentEntityInternal = $scope.currentEntity;
|
||||
|
||||
$scope.lazyLoad = function() {
|
||||
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
||||
|
@ -2763,7 +2764,9 @@ quayApp.directive('entitySearch', function () {
|
|||
};
|
||||
|
||||
$scope.clearEntityInternal = function() {
|
||||
$scope.currentEntityInternal = null;
|
||||
$scope.currentEntity = null;
|
||||
|
||||
if ($scope.entitySelected) {
|
||||
$scope.entitySelected(null);
|
||||
}
|
||||
|
@ -2777,6 +2780,7 @@ quayApp.directive('entitySearch', function () {
|
|||
}
|
||||
|
||||
if ($scope.isPersistent) {
|
||||
$scope.currentEntityInternal = entity;
|
||||
$scope.currentEntity = entity;
|
||||
}
|
||||
|
||||
|
@ -2901,6 +2905,16 @@ quayApp.directive('entitySearch', function () {
|
|||
$scope.$watch('inputTitle', function(title) {
|
||||
input.setAttribute('placeholder', title);
|
||||
});
|
||||
|
||||
$scope.$watch('currentEntity', function(entity) {
|
||||
if ($scope.currentEntityInternal != entity) {
|
||||
if (entity) {
|
||||
$scope.setEntityInternal(entity, false);
|
||||
} else {
|
||||
$scope.clearEntityInternal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -3438,6 +3452,145 @@ quayApp.directive('dropdownSelectMenu', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('setupTriggerDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: '/static/directives/setup-trigger-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'trigger': '=trigger',
|
||||
'counter': '=counter',
|
||||
'canceled': '&canceled',
|
||||
'activated': '&activated'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, UserService) {
|
||||
$scope.show = function() {
|
||||
$scope.pullEntity = null;
|
||||
$scope.publicPull = true;
|
||||
$scope.showPullRequirements = false;
|
||||
|
||||
$('#setupTriggerModal').modal({});
|
||||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function() {
|
||||
$scope.cancelSetupTrigger();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isNamespaceAdmin = function(namespace) {
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
|
||||
$scope.cancelSetupTrigger = function() {
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
};
|
||||
|
||||
$scope.hide = function() {
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.setPublicPull = function(value) {
|
||||
$scope.publicPull = value;
|
||||
};
|
||||
|
||||
$scope.checkAnalyze = function(isValid) {
|
||||
if (!isValid) {
|
||||
$scope.publicPull = true;
|
||||
$scope.pullEntity = null;
|
||||
$scope.showPullRequirements = false;
|
||||
$scope.checkingPullRequirements = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.checkingPullRequirements = true;
|
||||
$scope.showPullRequirements = true;
|
||||
$scope.pullRequirements = null;
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': $scope.trigger.config
|
||||
};
|
||||
|
||||
ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
|
||||
$scope.pullRequirements = resp;
|
||||
|
||||
if (resp['status'] == 'publicbase') {
|
||||
$scope.publicPull = true;
|
||||
$scope.pullEntity = null;
|
||||
} else if (resp['namespace']) {
|
||||
$scope.publicPull = false;
|
||||
|
||||
if (resp['robots'] && resp['robots'].length > 0) {
|
||||
$scope.pullEntity = resp['robots'][0];
|
||||
} else {
|
||||
$scope.pullEntity = null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.checkingPullRequirements = false;
|
||||
}, function(resp) {
|
||||
$scope.pullRequirements = resp;
|
||||
$scope.checkingPullRequirements = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.activate = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': $scope.trigger['config']
|
||||
};
|
||||
|
||||
if ($scope.pullEntity) {
|
||||
data['pull_robot'] = $scope.pullEntity['name'];
|
||||
}
|
||||
|
||||
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
||||
trigger['is_active'] = true;
|
||||
trigger['pull_robot'] = resp['pull_robot'];
|
||||
$scope.activated({'trigger': $scope.trigger});
|
||||
}, function(resp) {
|
||||
$scope.hide();
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
|
||||
bootbox.dialog({
|
||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
||||
"title": "Could not activate build trigger",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var check = function() {
|
||||
if ($scope.counter && $scope.trigger && $scope.repository) {
|
||||
$scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('trigger', check);
|
||||
$scope.$watch('counter', check);
|
||||
$scope.$watch('repository', check);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
|
||||
quayApp.directive('triggerSetupGithub', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -3447,15 +3600,18 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'trigger': '=trigger'
|
||||
'trigger': '=trigger',
|
||||
'analyze': '&analyze'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.analyzeCounter = 0;
|
||||
$scope.setupReady = false;
|
||||
$scope.loading = true;
|
||||
|
||||
$scope.handleLocationInput = function(location) {
|
||||
$scope.trigger['config']['subdir'] = location || '';
|
||||
$scope.isInvalidLocation = $scope.locations.indexOf(location) < 0;
|
||||
$scope.analyze({'isValid': !$scope.isInvalidLocation});
|
||||
};
|
||||
|
||||
$scope.handleLocationSelected = function(datum) {
|
||||
|
@ -3466,6 +3622,7 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
$scope.currentLocation = location;
|
||||
$scope.trigger['config']['subdir'] = location || '';
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': true});
|
||||
};
|
||||
|
||||
$scope.selectRepo = function(repo, org) {
|
||||
|
@ -3504,6 +3661,7 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
$scope.locations = null;
|
||||
$scope.trigger.$ready = false;
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': false});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3516,12 +3674,14 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
} else {
|
||||
$scope.currentLocation = null;
|
||||
$scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
|
||||
$scope.analyze({'isValid': !$scope.isInvalidLocation});
|
||||
}
|
||||
}, function(resp) {
|
||||
$scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
|
||||
$scope.locations = null;
|
||||
$scope.trigger.$ready = false;
|
||||
$scope.isInvalidLocation = false;
|
||||
$scope.analyze({'isValid': false});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -3566,7 +3726,14 @@ quayApp.directive('triggerSetupGithub', function () {
|
|||
});
|
||||
};
|
||||
|
||||
loadSources();
|
||||
var check = function() {
|
||||
if ($scope.repository && $scope.trigger) {
|
||||
loadSources();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('repository', check);
|
||||
$scope.$watch('trigger', check);
|
||||
|
||||
$scope.$watch('currentRepo', function(repo) {
|
||||
$scope.selectRepoInternal(repo);
|
||||
|
|
|
@ -1165,6 +1165,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||
$scope.githubClientId = KeyService.githubClientId;
|
||||
|
||||
$scope.showTriggerSetupCounter = 0;
|
||||
|
||||
$scope.getBadgeFormat = function(format, repo) {
|
||||
if (!repo) { return; }
|
||||
|
||||
|
@ -1454,65 +1456,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
|
|||
};
|
||||
|
||||
$scope.setupTrigger = function(trigger) {
|
||||
$scope.triggerSetupReady = false;
|
||||
$scope.currentSetupTrigger = trigger;
|
||||
|
||||
trigger['_pullEntity'] = null;
|
||||
trigger['_publicPull'] = true;
|
||||
|
||||
$('#setupTriggerModal').modal({});
|
||||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
||||
$scope.$apply(function() {
|
||||
$scope.cancelSetupTrigger();
|
||||
});
|
||||
});
|
||||
$scope.showTriggerSetupCounter++;
|
||||
};
|
||||
|
||||
$scope.isNamespaceAdmin = function(namespace) {
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
$scope.cancelSetupTrigger = function(trigger) {
|
||||
if ($scope.currentSetupTrigger != trigger) { return; }
|
||||
|
||||
$scope.finishSetupTrigger = function(trigger) {
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
$scope.currentSetupTrigger = null;
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'trigger_uuid': trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': trigger['config']
|
||||
};
|
||||
|
||||
if (trigger['_pullEntity']) {
|
||||
data['pull_robot'] = trigger['_pullEntity']['name'];
|
||||
}
|
||||
|
||||
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
||||
trigger['is_active'] = true;
|
||||
trigger['pull_robot'] = resp['pull_robot'];
|
||||
}, function(resp) {
|
||||
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1);
|
||||
bootbox.dialog({
|
||||
"message": resp['data']['message'] || 'The build trigger setup could not be completed',
|
||||
"title": "Could not activate build trigger",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancelSetupTrigger = function() {
|
||||
if (!$scope.currentSetupTrigger) { return; }
|
||||
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
$scope.deleteTrigger($scope.currentSetupTrigger);
|
||||
$scope.currentSetupTrigger = null;
|
||||
$scope.deleteTrigger(trigger);
|
||||
};
|
||||
|
||||
$scope.startTrigger = function(trigger) {
|
||||
|
|
|
@ -377,76 +377,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Auth dialog -->
|
||||
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
|
||||
shown="!!shownToken" counter="shownTokenCounter">
|
||||
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="setupTriggerModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Setup new build trigger</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="trigger-option-section">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td style="width: 114px">
|
||||
<div class="context-tooltip" data-title="The credentials used by the builder when pulling images" bs-tooltip>
|
||||
Pull Credentials:
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-if="!isNamespaceAdmin(repo.namespace)" style="color: #aaa;">
|
||||
In order to set pull credentials for a build trigger, you must be an Administrator of the namespace <strong>{{ repo.namespace }}</strong>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" ng-if="isNamespaceAdmin(repo.namespace)">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="currentSetupTrigger._publicPull ? 'active btn-info' : ''" ng-click="currentSetupTrigger._publicPull = true">Public</button>
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="currentSetupTrigger._publicPull ? '' : 'active btn-info'" ng-click="currentSetupTrigger._publicPull = false">
|
||||
<i class="fa fa-wrench"></i>
|
||||
Robot account
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="!currentSetupTrigger._publicPull">
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div class="entity-search" namespace="repo.namespace" include-teams="false"
|
||||
input-title="'Select robot account for pulling...'"
|
||||
is-organization="repo.is_organization"
|
||||
is-persistent="true"
|
||||
current-entity="currentSetupTrigger._pullEntity"
|
||||
filter="['robot']"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="trigger-description-element trigger-option-section" ng-switch on="currentSetupTrigger.service">
|
||||
<div ng-switch-when="github">
|
||||
<div class="trigger-setup-github" repository="repo" trigger="currentSetupTrigger"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-disabled="!currentSetupTrigger.$ready || (!currentSetupTrigger._publicPull && !currentSetupTrigger._pullEntity)"
|
||||
ng-click="finishSetupTrigger(currentSetupTrigger)">Finished</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<!-- Setup trigger dialog-->
|
||||
<div class="setup-trigger-dialog" repository="repo"
|
||||
trigger="currentSetupTrigger"
|
||||
canceled="cancelSetupTrigger(trigger)"
|
||||
counter="showTriggerSetupCounter"></div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotchangeModal">
|
||||
|
|
Binary file not shown.
|
@ -17,7 +17,7 @@ from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, Reposi
|
|||
from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList)
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
from endpoints.api.webhook import Webhook, WebhookList
|
||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
||||
Signin, User, UserAuthorizationList, UserAuthorization)
|
||||
|
@ -87,6 +87,9 @@ class ApiTestCase(unittest.TestCase):
|
|||
|
||||
rv = client.open(final_url, **open_kwargs)
|
||||
msg = '%s %s: %s expected: %s' % (method, final_url, rv.status_code, expected_status)
|
||||
if rv.status_code != expected_status:
|
||||
print rv.data
|
||||
|
||||
self.assertEqual(rv.status_code, expected_status, msg)
|
||||
|
||||
def setUp(self):
|
||||
|
@ -1198,6 +1201,130 @@ class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
|
|||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', None)
|
||||
|
||||
class TestActivateBuildTrigger0byeDevtableShared(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(ActivateBuildTrigger, trigger_uuid="0BYE", repository="devtable/shared")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', None)
|
||||
|
||||
|
||||
class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(ActivateBuildTrigger, trigger_uuid="0BYE", repository="buynlarge/orgrepo")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', None)
|
||||
|
||||
|
||||
class TestBuildTriggerAnalyze0byePublicPublicrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="public/publicrepo")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 403, 'devtable', {'config': {}})
|
||||
|
||||
|
||||
class TestBuildTriggerAnalyze0byeDevtableShared(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="devtable/shared")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||
|
||||
|
||||
class TestBuildTriggerAnalyze0byeBuynlargeOrgrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="buynlarge/orgrepo")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||
|
||||
class TestBuildTriggerAnalyze0byeDevtableShared(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="devtable/shared")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||
|
||||
|
||||
class TestBuildTriggerAnalyze0byeBuynlargeOrgrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(BuildTriggerAnalyze, trigger_uuid="0BYE", repository="buynlarge/orgrepo")
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, None)
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', None)
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', None)
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', {'config': {}})
|
||||
|
||||
|
||||
class TestRepositoryImageChangesPtsgPublicPublicrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -19,7 +19,7 @@ from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildLogs, Repo
|
|||
from endpoints.api.robot import UserRobotList, OrgRobot, OrgRobotList, UserRobot
|
||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
BuildTriggerList)
|
||||
BuildTriggerList, BuildTriggerAnalyze)
|
||||
from endpoints.api.webhook import Webhook, WebhookList
|
||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
||||
UserAuthorizationList, UserAuthorization)
|
||||
|
@ -1623,6 +1623,15 @@ class FakeBuildTrigger(BuildTriggerBase):
|
|||
def manual_start(self, auth_token, config):
|
||||
return ('foo', ['bar'], 'build-name', 'subdir')
|
||||
|
||||
def dockerfile_url(self, auth_token, config):
|
||||
return 'http://some/url'
|
||||
|
||||
def load_dockerfile_contents(self, auth_token, config):
|
||||
if not 'dockerfile' in config:
|
||||
return None
|
||||
|
||||
return config['dockerfile']
|
||||
|
||||
|
||||
class TestBuildTriggers(ApiTestCase):
|
||||
def test_list_build_triggers(self):
|
||||
|
@ -1671,6 +1680,82 @@ class TestBuildTriggers(ApiTestCase):
|
|||
self.assertEquals(0, len(json['triggers']))
|
||||
|
||||
|
||||
def test_analyze_fake_trigger(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
database.BuildTriggerService.create(name='fakeservice')
|
||||
|
||||
# Add a new fake trigger.
|
||||
repo = model.get_repository(ADMIN_ACCESS_USER, 'simple')
|
||||
user = model.get_user(ADMIN_ACCESS_USER)
|
||||
trigger = model.create_build_trigger(repo, 'fakeservice', 'sometoken', user)
|
||||
|
||||
# Analyze the trigger's dockerfile: First, no dockerfile.
|
||||
trigger_config = {}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('error', analyze_json['status'])
|
||||
self.assertEquals('Could not read the Dockerfile for the trigger', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Second, missing FROM in dockerfile.
|
||||
trigger_config = {'dockerfile': 'MAINTAINER me'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('warning', analyze_json['status'])
|
||||
self.assertEquals('No FROM line found in the Dockerfile', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Third, dockerfile with public repo.
|
||||
trigger_config = {'dockerfile': 'FROM somerepo'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('publicbase', analyze_json['status'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Fourth, dockerfile with private repo with an invalid path.
|
||||
trigger_config = {'dockerfile': 'FROM localhost:5000/somepath'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('warning', analyze_json['status'])
|
||||
self.assertEquals('"localhost:5000/somepath" is not a valid Quay repository path', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Fifth, dockerfile with private repo that does not exist.
|
||||
trigger_config = {'dockerfile': 'FROM localhost:5000/nothere/randomrepo'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('error', analyze_json['status'])
|
||||
self.assertEquals('Repository "localhost:5000/nothere/randomrepo" was not found', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Sixth, dockerfile with private repo that the user cannot see.
|
||||
trigger_config = {'dockerfile': 'FROM localhost:5000/randomuser/randomrepo'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('error', analyze_json['status'])
|
||||
self.assertEquals('Repository "localhost:5000/randomuser/randomrepo" was not found', analyze_json['message'])
|
||||
|
||||
# Analyze the trigger's dockerfile: Seventh, dockerfile with private repo that the user see.
|
||||
trigger_config = {'dockerfile': 'FROM localhost:5000/devtable/complex'}
|
||||
analyze_json = self.postJsonResponse(BuildTriggerAnalyze,
|
||||
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
|
||||
data={'config': trigger_config})
|
||||
|
||||
self.assertEquals('analyzed', analyze_json['status'])
|
||||
self.assertEquals('devtable', analyze_json['namespace'])
|
||||
self.assertEquals('complex', analyze_json['name'])
|
||||
self.assertEquals(False, analyze_json['is_public'])
|
||||
self.assertEquals(ADMIN_ACCESS_USER + '+dtrobot', analyze_json['robots'][0]['name'])
|
||||
|
||||
|
||||
def test_fake_trigger(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
|
|
72
util/dockerfileparse.py
Normal file
72
util/dockerfileparse.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
import re
|
||||
|
||||
LINE_CONTINUATION_REGEX = re.compile('\s*\\\s*\n')
|
||||
COMMAND_REGEX = re.compile('([A-Z]+)\s(.*)')
|
||||
|
||||
COMMENT_CHARACTER = '#'
|
||||
|
||||
class ParsedDockerfile(object):
|
||||
def __init__(self, commands):
|
||||
self.commands = commands
|
||||
|
||||
def get_commands_of_kind(self, kind):
|
||||
return [command for command in self.commands if command['command'] == kind]
|
||||
|
||||
def get_base_image(self):
|
||||
image_and_tag = self.get_base_image_and_tag()
|
||||
if not image_and_tag:
|
||||
return None
|
||||
|
||||
# Note:
|
||||
# Dockerfile images references can be of multiple forms:
|
||||
# server:port/some/path
|
||||
# somepath
|
||||
# server/some/path
|
||||
# server/some/path:tag
|
||||
# server:port/some/path:tag
|
||||
parts = image_and_tag.strip().split(':')
|
||||
|
||||
if len(parts) == 1:
|
||||
# somepath
|
||||
return parts[0]
|
||||
|
||||
# Otherwise, determine if the last part is a port
|
||||
# or a tag.
|
||||
if parts[-1].find('/') >= 0:
|
||||
# Last part is part of the hostname.
|
||||
return image_and_tag
|
||||
|
||||
return '/'.join(parts[0:-1])
|
||||
|
||||
def get_base_image_and_tag(self):
|
||||
from_commands = self.get_commands_of_kind('FROM')
|
||||
if not from_commands:
|
||||
return None
|
||||
|
||||
return from_commands[0]['parameters']
|
||||
|
||||
|
||||
def strip_comments(contents):
|
||||
lines = [line for line in contents.split('\n') if not line.startswith(COMMENT_CHARACTER)]
|
||||
return '\n'.join(lines)
|
||||
|
||||
def join_continued_lines(contents):
|
||||
return LINE_CONTINUATION_REGEX.sub('', contents)
|
||||
|
||||
def parse_dockerfile(contents):
|
||||
contents = join_continued_lines(strip_comments(contents))
|
||||
lines = [line for line in contents.split('\n') if len(line) > 0]
|
||||
|
||||
commands = []
|
||||
for line in lines:
|
||||
m = COMMAND_REGEX.match(line)
|
||||
if m:
|
||||
command = m.group(1)
|
||||
parameters = m.group(2)
|
||||
|
||||
commands.append({
|
||||
'command': command,
|
||||
'parameters': parameters
|
||||
})
|
||||
|
||||
return ParsedDockerfile(commands)
|
Reference in a new issue