2015-02-20 23:15:48 +00:00
|
|
|
(function() {
|
|
|
|
/**
|
|
|
|
* Repository Build view page. Displays the status of a repository build.
|
|
|
|
*/
|
|
|
|
angular.module('quayPages').config(['pages', function(pages) {
|
|
|
|
pages.create('repo-build', 'repo-build.html', RepoBuildCtrl);
|
|
|
|
}]);
|
|
|
|
|
|
|
|
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize,
|
|
|
|
ansi2html, AngularViewArray, AngularPollChannel) {
|
|
|
|
var namespace = $routeParams.namespace;
|
|
|
|
var name = $routeParams.name;
|
|
|
|
|
|
|
|
// Watch for changes to the current parameter.
|
|
|
|
$scope.$on('$routeUpdate', function(){
|
|
|
|
if ($location.search().current) {
|
|
|
|
$scope.setCurrentBuild($location.search().current, false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
$scope.builds = null;
|
|
|
|
$scope.pollChannel = null;
|
|
|
|
$scope.buildDialogShowCounter = 0;
|
|
|
|
|
|
|
|
$scope.showNewBuildDialog = function() {
|
|
|
|
$scope.buildDialogShowCounter++;
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.handleBuildStarted = function(newBuild) {
|
|
|
|
if (!$scope.builds) { return; }
|
|
|
|
|
|
|
|
$scope.builds.unshift(newBuild);
|
|
|
|
$scope.setCurrentBuild(newBuild['id'], true);
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.adjustLogHeight = function() {
|
|
|
|
var triggerOffset = 0;
|
|
|
|
if ($scope.currentBuild && $scope.currentBuild.trigger) {
|
|
|
|
triggerOffset = 85;
|
|
|
|
}
|
|
|
|
$('.build-logs').height($(window).height() - 415 - triggerOffset);
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.askRestartBuild = function(build) {
|
|
|
|
$('#confirmRestartBuildModal').modal({});
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.askCancelBuild = function(build) {
|
|
|
|
bootbox.confirm('Are you sure you want to cancel this build?', function(r) {
|
|
|
|
if (r) {
|
|
|
|
var params = {
|
|
|
|
'repository': namespace + '/' + name,
|
|
|
|
'build_uuid': build.id
|
|
|
|
};
|
|
|
|
|
|
|
|
ApiService.cancelRepoBuild(null, params).then(function() {
|
|
|
|
if (!$scope.builds) { return; }
|
|
|
|
$scope.builds.splice($.inArray(build, $scope.builds), 1);
|
|
|
|
|
|
|
|
if ($scope.builds.length) {
|
|
|
|
$scope.currentBuild = $scope.builds[0];
|
|
|
|
} else {
|
|
|
|
$scope.currentBuild = null;
|
|
|
|
}
|
|
|
|
}, ApiService.errorDisplay('Cannot cancel build'));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.restartBuild = function(build) {
|
|
|
|
$('#confirmRestartBuildModal').modal('hide');
|
|
|
|
|
|
|
|
var subdirectory = '';
|
|
|
|
if (build['job_config']) {
|
|
|
|
subdirectory = build['job_config']['build_subdir'] || '';
|
|
|
|
}
|
|
|
|
|
|
|
|
var data = {
|
|
|
|
'file_id': build['resource_key'],
|
|
|
|
'subdirectory': subdirectory,
|
|
|
|
'docker_tags': build['job_config']['docker_tags']
|
|
|
|
};
|
|
|
|
|
|
|
|
if (build['pull_robot']) {
|
|
|
|
data['pull_robot'] = build['pull_robot']['name'];
|
|
|
|
}
|
|
|
|
|
|
|
|
var params = {
|
|
|
|
'repository': namespace + '/' + name
|
|
|
|
};
|
|
|
|
|
|
|
|
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
|
|
|
|
if (!$scope.builds) { return; }
|
|
|
|
|
|
|
|
$scope.builds.unshift(newBuild);
|
|
|
|
$scope.setCurrentBuild(newBuild['id'], true);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.hasLogs = function(container) {
|
|
|
|
return container.logs.hasEntries;
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.setCurrentBuild = function(buildId, opt_updateURL) {
|
|
|
|
if (!$scope.builds) { return; }
|
|
|
|
|
|
|
|
// Find the build.
|
|
|
|
for (var i = 0; i < $scope.builds.length; ++i) {
|
|
|
|
if ($scope.builds[i].id == buildId) {
|
|
|
|
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.processANSI = function(message, container) {
|
|
|
|
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
|
|
|
|
|
|
|
|
// Note: order is important here.
|
|
|
|
var setup = filter.getSetupHtml();
|
|
|
|
var stream = filter.addInputToStream(message);
|
|
|
|
var teardown = filter.getTeardownHtml();
|
|
|
|
return setup + stream + teardown;
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
|
|
|
|
if (build == $scope.currentBuild) { return; }
|
|
|
|
|
|
|
|
$scope.logEntries = null;
|
|
|
|
$scope.logStartIndex = null;
|
|
|
|
$scope.currentParentEntry = null;
|
|
|
|
|
|
|
|
$scope.currentBuild = build;
|
|
|
|
|
|
|
|
if (opt_updateURL) {
|
|
|
|
if (build) {
|
|
|
|
$location.search('current', build.id);
|
|
|
|
} else {
|
|
|
|
$location.search('current', null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Timeout needed to ensure the log element has been created
|
|
|
|
// before its height is adjusted.
|
|
|
|
setTimeout(function() {
|
|
|
|
$scope.adjustLogHeight();
|
|
|
|
}, 1);
|
|
|
|
|
|
|
|
// Stop any existing polling.
|
|
|
|
if ($scope.pollChannel) {
|
|
|
|
$scope.pollChannel.stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a new channel for polling the build status and logs.
|
|
|
|
var conductStatusAndLogRequest = function(callback) {
|
|
|
|
getBuildStatusAndLogs(build, callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
$scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
|
|
|
|
$scope.pollChannel.start();
|
|
|
|
};
|
|
|
|
|
|
|
|
var processLogs = function(logs, startIndex, endIndex) {
|
|
|
|
if (!$scope.logEntries) { $scope.logEntries = []; }
|
|
|
|
|
|
|
|
// If the start index given is less than that requested, then we've received a larger
|
|
|
|
// pool of logs, and we need to only consider the new ones.
|
|
|
|
if (startIndex < $scope.logStartIndex) {
|
|
|
|
logs = logs.slice($scope.logStartIndex - startIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = 0; i < logs.length; ++i) {
|
|
|
|
var entry = logs[i];
|
|
|
|
var type = entry['type'] || 'entry';
|
|
|
|
if (type == 'command' || type == 'phase' || type == 'error') {
|
|
|
|
entry['logs'] = AngularViewArray.create();
|
|
|
|
entry['index'] = $scope.logStartIndex + i;
|
|
|
|
|
|
|
|
$scope.logEntries.push(entry);
|
|
|
|
$scope.currentParentEntry = entry;
|
|
|
|
} else if ($scope.currentParentEntry) {
|
|
|
|
$scope.currentParentEntry['logs'].push(entry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return endIndex;
|
|
|
|
};
|
|
|
|
|
2015-04-22 19:16:59 +00:00
|
|
|
var handleLogsData = function(logsData, callback) {
|
|
|
|
// Process the logs we've received.
|
|
|
|
$scope.logStartIndex = processLogs(logsData['logs'], logsData['start'], logsData['total']);
|
|
|
|
|
|
|
|
// If the build status is an error, open the last two log entries.
|
|
|
|
var currentBuild = $scope.currentBuild;
|
|
|
|
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
|
|
|
|
var openLogEntries = function(entry) {
|
|
|
|
if (entry.logs) {
|
|
|
|
entry.logs.setVisible(true);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
|
|
|
|
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the build phase is an error or a complete, then we mark the channel
|
|
|
|
// as closed.
|
|
|
|
callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete');
|
|
|
|
};
|
|
|
|
|
2015-02-20 23:15:48 +00:00
|
|
|
var getBuildStatusAndLogs = function(build, callback) {
|
|
|
|
var params = {
|
|
|
|
'repository': namespace + '/' + name,
|
|
|
|
'build_uuid': build.id
|
|
|
|
};
|
|
|
|
|
|
|
|
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
|
|
|
if (build != $scope.currentBuild) { callback(false); return; }
|
|
|
|
|
|
|
|
// Note: We use extend here rather than replacing as Angular is depending on the
|
|
|
|
// root build object to remain the same object.
|
|
|
|
var matchingBuilds = $.grep($scope.builds, function(elem) {
|
|
|
|
return elem['id'] == resp['id']
|
|
|
|
});
|
|
|
|
|
|
|
|
var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null;
|
|
|
|
if (currentBuild) {
|
|
|
|
currentBuild = $.extend(true, currentBuild, resp);
|
|
|
|
} else {
|
|
|
|
currentBuild = resp;
|
|
|
|
$scope.builds.push(currentBuild);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the updated logs for the build.
|
|
|
|
var options = {
|
|
|
|
'start': $scope.logStartIndex
|
|
|
|
};
|
|
|
|
|
|
|
|
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
|
|
|
if (build != $scope.currentBuild) { callback(false); return; }
|
|
|
|
|
2015-04-22 19:16:59 +00:00
|
|
|
// If we get a logs url back, then we need to make another XHR request to retrieve the
|
|
|
|
// data.
|
|
|
|
if (resp['logs_url']) {
|
|
|
|
$.ajax({
|
|
|
|
url: resp['logs_url'],
|
|
|
|
}).done(function(r) {
|
|
|
|
handleLogsData(r, callback);
|
|
|
|
});
|
|
|
|
return;
|
2015-02-20 23:15:48 +00:00
|
|
|
}
|
|
|
|
|
2015-04-22 19:16:59 +00:00
|
|
|
handleLogsData(resp, callback);
|
2015-02-20 23:15:48 +00:00
|
|
|
}, function() {
|
|
|
|
callback(false);
|
|
|
|
});
|
|
|
|
}, function() {
|
|
|
|
callback(false);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
var fetchRepository = function() {
|
|
|
|
var params = {'repository': namespace + '/' + name};
|
|
|
|
$rootScope.title = 'Loading Repository...';
|
|
|
|
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
|
|
|
if (!repo.can_write) {
|
|
|
|
$rootScope.title = 'Unknown builds';
|
|
|
|
$scope.accessDenied = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$rootScope.title = 'Repository Builds';
|
|
|
|
$scope.repo = repo;
|
|
|
|
|
|
|
|
getBuildInfo();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
var getBuildInfo = function(repo) {
|
|
|
|
var params = {
|
|
|
|
'repository': namespace + '/' + name
|
|
|
|
};
|
|
|
|
|
|
|
|
ApiService.getRepoBuilds(null, params).then(function(resp) {
|
|
|
|
$scope.builds = resp.builds;
|
|
|
|
|
|
|
|
if ($location.search().current) {
|
|
|
|
$scope.setCurrentBuild($location.search().current, false);
|
|
|
|
} else if ($scope.builds.length > 0) {
|
|
|
|
$scope.setCurrentBuild($scope.builds[0].id, true);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
fetchRepository();
|
|
|
|
}
|
|
|
|
})();
|