Add ability to see a build's build pack, including browsing and downloading of the contents if it is a zip

This commit is contained in:
Joseph Schorr 2014-02-17 17:28:20 -05:00
parent 7bf6936154
commit bc0d51656a
12 changed files with 936 additions and 637 deletions

View file

@ -1149,7 +1149,7 @@ def get_repo(namespace, repository):
abort(403) # Permission denied
def build_status_view(build_obj):
def build_status_view(build_obj, can_write=False):
status = build_logs.get_status(build_obj.uuid)
return {
'id': build_obj.uuid,
@ -1157,7 +1157,8 @@ def build_status_view(build_obj):
'started': build_obj.started,
'display_name': build_obj.display_name,
'status': status,
'resource_key': build_obj.resource_key
'resource_key': build_obj.resource_key if can_write else None,
'is_writer': can_write
}
@ -1167,9 +1168,10 @@ def get_repo_builds(namespace, repository):
permission = ReadRepositoryPermission(namespace, repository)
is_public = model.repository_is_public(namespace, repository)
if permission.can() or is_public:
can_write = ModifyRepositoryPermission(namespace, repository).can()
builds = model.list_repository_builds(namespace, repository)
return jsonify({
'builds': [build_status_view(build) for build in builds]
'builds': [build_status_view(build, can_write) for build in builds]
})
abort(403) # Permission denied
@ -1183,7 +1185,29 @@ def get_repo_build_status(namespace, repository, build_uuid):
is_public = model.repository_is_public(namespace, repository)
if permission.can() or is_public:
build = model.get_repository_build(namespace, repository, build_uuid)
return jsonify(build_status_view(build))
if not build:
abort(404)
can_write = ModifyRepositoryPermission(namespace, repository).can()
return jsonify(build_status_view(build, can_write))
abort(403) # Permission denied
@api.route('/repository/<path:repository>/build/<build_uuid>/archiveurl',
methods=['GET'])
@parse_repository_name
def get_repo_build_archive_url(namespace, repository, build_uuid):
permission = ModifyRepositoryPermission(namespace, repository)
if permission.can():
build = model.get_repository_build(namespace, repository, build_uuid)
if not build:
abort(404)
url = user_files.get_file_url(build.resource_key)
return jsonify({
'url': url
})
abort(403) # Permission denied
@ -1249,7 +1273,7 @@ def request_repo_build(namespace, repository):
{'repo': repository, 'namespace': namespace,
'fileid': dockerfile_id}, repo=repo)
resp = jsonify(build_status_view(build_request))
resp = jsonify(build_status_view(build_request, True))
repo_string = '%s/%s' % (namespace, repository)
resp.headers['Location'] = url_for('api.get_repo_build_status',
repository=repo_string,

View file

@ -277,8 +277,8 @@ def populate_database():
token = model.create_access_token(building, 'write')
tag = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name)
build = model.create_repository_build(building, token, '123-45-6789', tag,
'build-name')
build = model.create_repository_build(building, token, '701dcc3724fb4f2ea6c31400528343cd',
tag, 'build-name')
build.uuid = 'deadbeef-dead-beef-dead-beefdeadbeef'
build.save()

View file

@ -2263,23 +2263,10 @@ p.editable:hover i {
overflow: hidden;
}
#changes-tree-container .node rect {
cursor: pointer;
fill: #fff;
fill-opacity: 1;
stroke: #fff;
stroke-width: 1.5px;
}
#changes-tree-container .node .change-icon {
font-size: 14px;
}
#changes-tree-container .node text {
font: 12px sans-serif;
pointer-events: none;
}
#changes-tree-container .node.changed text {
fill: rgb(73, 100, 209);
}
@ -2293,7 +2280,20 @@ p.editable:hover i {
fill: rgb(209, 73, 73);
}
#changes-tree-container path.link {
.file-tree-base .node rect {
cursor: pointer;
fill: #fff;
fill-opacity: 1;
stroke: #fff;
stroke-width: 1.5px;
}
.file-tree-base .node text {
font: 12px sans-serif;
pointer-events: none;
}
.file-tree-base path.link {
fill: none;
stroke: #9ecae1;
stroke-width: 1.5px;

View file

@ -791,6 +791,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/build', {templateUrl: '/static/partials/repo-build.html', controller:RepoBuildCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/build/:buildid/buildpack', {templateUrl: '/static/partials/build-package.html', controller:BuildPackageCtrl, reloadOnSearch: false}).
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html',

View file

@ -774,6 +774,128 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
loadViewInfo();
}
function BuildPackageCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $timeout) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var buildid = $routeParams.buildid;
var params = {
'repository': namespace + '/' + name,
'build_uuid': buildid
};
$scope.initializeTree = function() {
if ($scope.drawn) { return; }
$scope.drawn = true;
$timeout(function() {
$scope.tree.draw('file-tree-container');
}, 10);
};
var processBuildPack = function(response) {
// Try to load as a zip file.
var zipFiles = null;
var zip = null;
try {
var zip = new JSZip(response);
zipFiles = zip.files;
} catch (e) {
}
// Find the Dockerfile in the zip file. If there isn't any zip file, then the response
// itself (should) be the Dockerfile.
if (zipFiles && Object.keys(zipFiles).length) {
// Load the dockerfile contents.
var dockerfile = zip.file('Dockerfile');
if (dockerfile) {
$scope.dockerFileContents = dockerfile.asText();
}
// Build the zip file tree.
$scope.tree = new FileTree(Object.keys(zipFiles));
$($scope.tree).bind('fileClicked', function(e) {
var file = zip.file(e.path);
if (file) {
var blob = new Blob([file.asArrayBuffer()]);
saveAs(blob, file.name);
}
});
} else {
$scope.dockerFileContents = response;
}
$scope.loaded = true;
};
var downloadBuildPack = function() {
$scope.downloadProgress = 0;
$scope.downloading = true;
ApiService.getRepoBuildArchiveUrl(null, params).then(function(resp) {
startDownload(resp['url']);
}, function() {
$scope.downloading = false;
$scope.downloadError = true;
});
};
var startDownload = function(url) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
if (request.overrideMimeType) {
request.overrideMimeType('text/plain; charset=x-user-defined');
}
request.onprogress = function(e) {
$scope.$apply(function() {
var percentLoaded;
if (e.lengthComputable) {
$scope.downloadProgress = (e.loaded / e.total) * 100;
}
});
};
request.onerror = function() {
$scope.$apply(function() {
$scope.downloading = false;
$scope.downloadError = true;
});
};
request.onreadystatechange = function() {
var state = request.readyState;
if (state == 4) {
$scope.$apply(function() {
$scope.downloading = false;
processBuildPack(request.responseText);
});
return;
}
};
request.send();
};
var getBuildInfo = function() {
$scope.repository_build = ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
if (!resp['is_writer']) {
$rootScope.title = 'Unknown build';
$scope.accessDenied = true;
return;
}
$rootScope.title = 'Repository Build Pack - ' + resp['display_name'];
$scope.repobuild = resp;
$scope.repo = {
'namespace': namespace,
'name': name
};
downloadBuildPack();
return resp;
});
};
getBuildInfo();
}
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
@ -805,7 +927,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
};
$scope.adjustLogHeight = function() {
$('.build-logs').height($(window).height() - 385);
$('.build-logs').height($(window).height() - 415);
};
$scope.askRestartBuild = function(build) {

File diff suppressed because it is too large Load diff

14
static/lib/jszip.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,45 @@
<div class="resource-view" resource="repository_build" error-message="'No matching repository build found'"></div>
<div class="container repo repo-build" ng-show="accessDenied">
You do not have permission to view this page
</div>
<div class="container repo repo-build repo-build-pack" ng-show="repobuild">
<div class="header row">
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/build' }}" class="back"><i class="fa fa-chevron-left"></i></a>
<h3>
<span class="repo-circle no-background" repo="repo"></span>
<span class="repo-breadcrumb" repo="repo" subsection-icon="'fa-tasks'" subsection="repobuild.display_name"></span>
</h3>
</div>
<div class="row" ng-show="downloading">
Downloading build pack:
<div class="progress" class="active progress-striped">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ downloadProgress }}" aria-valuemin="0" aria-valuemax="100" style="{{ 'width: ' + downloadProgress + '%' }}">
</div>
</div>
</div>
<div class="row" ng-show="downloadError">
Error: Could not download the build pack
</div>
<div class="row" ng-show="loaded">
<ul class="nav nav-tabs">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#dockerfile">Dockerfile</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()" ng-show="tree">All Files</a></li>
</ul>
<div class="tab-content">
<!-- Dockerfile view -->
<div class="tab-pane active" id="dockerfile">
<pre ng-show="dockerFileContents">{{ dockerFileContents }}</pre>
<span ng-show="!dockerFileContents">No Dockerfile found in the build pack</span>
</div>
<!-- File tree -->
<div class="tab-pane" id="tree">
<div id="file-tree-container" class="tree-container" onresize="tree && drawn && tree.notifyResized()"></div>
</div>
</div>
</div>
</div>

View file

@ -43,6 +43,12 @@
<div class="timing">
<i class="fa fa-clock-o"></i>
Started: <span am-time-ago="build.started || 0"></span>
<span style="display: inline-block; margin-left: 20px" ng-show="currentBuild.resource_key">
<i class="fa fa-archive"></i>
<a href="/repository/{{ repo.namespace }}/{{ repo.name }}/build/{{ currentBuild.id }}/buildpack"
style="display: inline-block; margin-left: 6px" bs-tooltip="tooltip.title"
title="View the uploaded build package for this build">Build Package</a>
</span>
</div>
<span class="phase-icon" ng-class="build.phase"></span>
<span class="build-message" phase="build.phase"></span>

View file

@ -23,8 +23,8 @@
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.2.0/js/bootstrap-datepicker.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script>
<script src="static/lib/nv.d3.min.js"></script>
<script src="static/lib/ZeroClipboard.min.js"></script>
@ -32,6 +32,7 @@
<script src="static/lib/d3-tip.js" charset="utf-8"></script>
<script src="static/lib/browser-chrome.js"></script>
<script src="static/lib/jszip.min.js"></script>
<script src="static/lib/Blob.js"></script>
<script src="static/lib/FileSaver.js"></script>
<script src="static/lib/jquery.base64.min.js"></script>

Binary file not shown.

View file

@ -250,7 +250,7 @@ class TestGetMatchingEntities(ApiTestCase):
json = self.getJsonResponse('api.get_matching_entities',
params=dict(prefix='o', namespace=ORGANIZATION,
includeTeams=True))
includeTeams='true'))
names = set([r['name'] for r in json['results']])
assert 'outsideorg' in names
@ -261,7 +261,7 @@ class TestGetMatchingEntities(ApiTestCase):
json = self.getJsonResponse('api.get_matching_entities',
params=dict(prefix='o', namespace=ORGANIZATION,
includeTeams=True))
includeTeams='true'))
names = set([r['name'] for r in json['results']])
assert 'outsideorg' in names
@ -684,12 +684,12 @@ class TestFindRepos(ApiTestCase):
class TestListRepos(ApiTestCase):
def test_listrepos_asguest(self):
json = self.getJsonResponse('api.list_repos', params=dict(public=True))
assert len(json['repositories']) == 0
json = self.getJsonResponse('api.list_repos', params=dict(public='true'))
assert len(json['repositories']) > 1
def test_listrepos_orgmember(self):
self.login(READ_ACCESS_USER)
json = self.getJsonResponse('api.list_repos', params=dict(public=True))
json = self.getJsonResponse('api.list_repos', params=dict(public='true'))
assert len(json['repositories']) > 1
def test_listrepos_filter(self):