diff --git a/data/buildlogs.py b/data/buildlogs.py index 07127b65f..f0101fd6e 100644 --- a/data/buildlogs.py +++ b/data/buildlogs.py @@ -61,8 +61,8 @@ class RedisBuildLogs(object): llen = self._redis.llen(self._logs_key(build_id)) log_entries = self._redis.lrange(self._logs_key(build_id), start_index, -1) return (llen, (json.loads(entry) for entry in log_entries)) - except redis.ConnectionError: - raise BuildStatusRetrievalError('Cannot retrieve build logs') + except redis.ConnectionError as ce: + raise BuildStatusRetrievalError('Cannot retrieve build logs: %s' % ce) def expire_log_entries(self, build_id): """ @@ -87,8 +87,8 @@ class RedisBuildLogs(object): """ try: fetched = self._redis.get(self._status_key(build_id)) - except redis.ConnectionError: - raise BuildStatusRetrievalError('Cannot retrieve build status') + except redis.ConnectionError as ce: + raise BuildStatusRetrievalError('Cannot retrieve build status: %s' % ce) return json.loads(fetched) if fetched else None diff --git a/endpoints/api/build.py b/endpoints/api/build.py index 972255955..133ad3616 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -8,11 +8,11 @@ import hashlib from flask import request from rfc3987 import parse as uri_parse -from app import app, userfiles as user_files, build_logs, log_archive, dockerfile_build_queue +from app import userfiles as user_files, build_logs, log_archive, dockerfile_build_queue from buildtrigger.basehandler import BuildTriggerHandler from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource, require_repo_read, require_repo_write, validate_json_request, - ApiResource, internal_only, format_date, api, path_param, + ApiResource, internal_only, format_date, api, path_param, require_repo_admin) from endpoints.exception import Unauthorized, NotFound, InvalidRequest from endpoints.building import start_build, PreparedBuild @@ -20,7 +20,8 @@ from data import database from data import model from auth.auth_context import get_authenticated_user from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, - AdministerRepositoryPermission, AdministerOrganizationPermission) + AdministerRepositoryPermission, AdministerOrganizationPermission, + SuperUserPermission) from data.buildlogs import BuildStatusRetrievalError from util.names import parse_robot_username @@ -87,26 +88,33 @@ def trigger_view(trigger, can_read=False, can_admin=False, for_build=False): def build_status_view(build_obj): phase = build_obj.phase + status = {} + error = None + try: status = build_logs.get_status(build_obj.uuid) - except BuildStatusRetrievalError: - status = {} + except BuildStatusRetrievalError as bsre: phase = 'cannot_load' + if SuperUserPermission().can(): + error = str(bsre) + else: + error = 'Redis may be down. Please contact support.' - # If the status contains a heartbeat, then check to see if has been written in the last few - # minutes. If not, then the build timed out. - if phase != database.BUILD_PHASE.COMPLETE and phase != database.BUILD_PHASE.ERROR: - if status is not None and 'heartbeat' in status and status['heartbeat']: - heartbeat = datetime.datetime.utcfromtimestamp(status['heartbeat']) - if datetime.datetime.utcnow() - heartbeat > datetime.timedelta(minutes=1): - phase = database.BUILD_PHASE.INTERNAL_ERROR + if phase != 'cannot_load': + # If the status contains a heartbeat, then check to see if has been written in the last few + # minutes. If not, then the build timed out. + if phase != database.BUILD_PHASE.COMPLETE and phase != database.BUILD_PHASE.ERROR: + if status is not None and 'heartbeat' in status and status['heartbeat']: + heartbeat = datetime.datetime.utcfromtimestamp(status['heartbeat']) + if datetime.datetime.utcnow() - heartbeat > datetime.timedelta(minutes=1): + phase = database.BUILD_PHASE.INTERNAL_ERROR - # If the phase is internal error, return 'error' instead if the number of retries - # on the queue item is 0. - if phase == database.BUILD_PHASE.INTERNAL_ERROR: - retry = build_obj.queue_id and dockerfile_build_queue.has_retries_remaining(build_obj.queue_id) - if not retry: - phase = database.BUILD_PHASE.ERROR + # If the phase is internal error, return 'error' instead if the number of retries + # on the queue item is 0. + if phase == database.BUILD_PHASE.INTERNAL_ERROR: + retry = build_obj.queue_id and dockerfile_build_queue.has_retries_remaining(build_obj.queue_id) + if not retry: + phase = database.BUILD_PHASE.ERROR repo_namespace = build_obj.repository.namespace_user.username repo_name = build_obj.repository.name @@ -134,7 +142,8 @@ def build_status_view(build_obj): 'repository': { 'namespace': repo_namespace, 'name': repo_name - } + }, + 'error': error, } if can_write: diff --git a/static/css/directives/ui/build-logs-view.css b/static/css/directives/ui/build-logs-view.css index a6e17bca5..e4da34a65 100644 --- a/static/css/directives/ui/build-logs-view.css +++ b/static/css/directives/ui/build-logs-view.css @@ -6,7 +6,7 @@ box-shadow: inset 10px 10px 5px -9px rgba(0,0,0,0.75); background-color: #263945; - min-height: 100px; + min-height: 74px; } .build-logs-view .co-alert { diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html index 12ad935ab..44a5d05ac 100644 --- a/static/directives/build-logs-view.html +++ b/static/directives/build-logs-view.html @@ -24,12 +24,17 @@ please check for JavaScript or networking issues and contact support. +
+ You are not authorized to view builds logs. Please have the owner of the repository grant you + admin access to this repository. +
+
Refreshing Build Status...
-
+
(Waiting for build to start) diff --git a/static/js/directives/ui/build-logs-view.js b/static/js/directives/ui/build-logs-view.js index a31fe253a..24d73903e 100644 --- a/static/js/directives/ui/build-logs-view.js +++ b/static/js/directives/ui/build-logs-view.js @@ -132,10 +132,16 @@ angular.module('quay').directive('buildLogsView', function () { } handleLogsData(resp, callback); - }, function() { + }, function(resp) { + if (resp.status == 403) { + $scope.loadError = 'unauthorized'; + } else { + $scope.loadError = 'request-failed'; + } callback(false); }); }, function() { + $scope.loadError = 'request-failed'; callback(false); }); }; diff --git a/static/js/directives/ui/build-message.js b/static/js/directives/ui/build-message.js index e0ac208d8..bf097d690 100644 --- a/static/js/directives/ui/build-message.js +++ b/static/js/directives/ui/build-message.js @@ -15,7 +15,7 @@ angular.module('quay').directive('buildMessage', function () { $scope.getBuildMessage = function (phase) { switch (phase) { case 'cannot_load': - return 'Cannot load build status - Please report this error'; + return 'Cannot load build status'; case 'starting': case 'initializing': diff --git a/static/lib/angular-moment.min.js b/static/lib/angular-moment.min.js index e74234d7a..7d1e1f518 100644 --- a/static/lib/angular-moment.min.js +++ b/static/lib/angular-moment.min.js @@ -1,2 +1,2 @@ -"format global";"deps angular";"deps moment";!function(){"use strict";function a(a,b){return a.module("angularMoment",[]).constant("angularMomentConfig",{preprocess:null,timezone:"",format:null,statefulFilters:!0}).constant("moment",b).constant("amTimeAgoConfig",{withoutSuffix:!1,serverTime:null,titleFormat:null}).directive("amTimeAgo",["$window","moment","amMoment","amTimeAgoConfig","angularMomentConfig",function(b,c,d,e,f){return function(g,h,i){function j(){var a;if(e.serverTime){var b=(new Date).getTime(),d=b-u+e.serverTime;a=c(d)}else a=c();return a}function k(){q&&(b.clearTimeout(q),q=null)}function l(a){if(h.text(a.from(j(),s)),t&&!h.attr("title")&&h.attr("title",a.local().format(t)),!x){var c=Math.abs(j().diff(a,"minute")),d=3600;1>c?d=1:60>c?d=30:180>c&&(d=300),q=b.setTimeout(function(){l(a)},1e3*d)}}function m(a){y&&h.attr("datetime",a)}function n(){if(k(),o){var a=d.preprocessDate(o,v,r);l(a),m(a.toISOString())}}var o,p,q=null,r=f.format,s=e.withoutSuffix,t=e.titleFormat,u=(new Date).getTime(),v=f.preprocess,w=i.amTimeAgo.replace(/^::/,""),x=0===i.amTimeAgo.indexOf("::"),y="TIME"===h[0].nodeName.toUpperCase();p=g.$watch(w,function(a){return"undefined"==typeof a||null===a||""===a?(k(),void(o&&(h.text(""),m(""),o=null))):(o=a,n(),void(void 0!==a&&x&&p()))}),a.isDefined(i.amWithoutSuffix)&&g.$watch(i.amWithoutSuffix,function(a){"boolean"==typeof a?(s=a,n()):s=e.withoutSuffix}),i.$observe("amFormat",function(a){"undefined"!=typeof a&&(r=a,n())}),i.$observe("amPreprocess",function(a){v=a,n()}),g.$on("$destroy",function(){k()}),g.$on("amMoment:localeChanged",function(){n()})}}]).service("amMoment",["moment","$rootScope","$log","angularMomentConfig",function(b,c,d,e){this.preprocessors={utc:b.utc,unix:b.unix},this.changeLocale=function(d,e){var f=b.locale(d,e);return a.isDefined(d)&&c.$broadcast("amMoment:localeChanged"),f},this.changeTimezone=function(a){e.timezone=a,c.$broadcast("amMoment:timezoneChanged")},this.preprocessDate=function(c,f,g){return a.isUndefined(f)&&(f=e.preprocess),this.preprocessors[f]?this.preprocessors[f](c,g):(f&&d.warn("angular-moment: Ignoring unsupported value for preprocess: "+f),!isNaN(parseFloat(c))&&isFinite(c)?b(parseInt(c,10)):b(c,g))},this.applyTimezone=function(a){var b=e.timezone;return a&&b&&(a.tz?a=a.tz(b):d.warn("angular-moment: timezone specified but moment.tz() is undefined. Did you forget to include moment-timezone.js?")),a}}]).filter("amCalendar",["moment","amMoment","angularMomentConfig",function(a,b,c){function d(c,d){if("undefined"==typeof c||null===c)return"";c=b.preprocessDate(c,d);var e=a(c);return e.isValid()?b.applyTimezone(e).calendar():""}return d.$stateful=c.statefulFilters,d}]).filter("amDateFormat",["moment","amMoment","angularMomentConfig",function(a,b,c){function d(c,d,e){if("undefined"==typeof c||null===c)return"";c=b.preprocessDate(c,e);var f=a(c);return f.isValid()?b.applyTimezone(f).format(d):""}return d.$stateful=c.statefulFilters,d}]).filter("amDurationFormat",["moment","angularMomentConfig",function(a,b){function c(b,c,d){return"undefined"==typeof b||null===b?"":a.duration(b,c).humanize(d)}return c.$stateful=b.statefulFilters,c}]).filter("amTimeAgo",["moment","amMoment","angularMomentConfig",function(a,b,c){function d(c,d,e){if("undefined"==typeof c||null===c)return"";c=b.preprocessDate(c,d);var f=a(c);return f.isValid()?b.applyTimezone(f).fromNow(e):""}return d.$stateful=c.statefulFilters,d}])}"function"==typeof define&&define.amd?define(["angular","moment"],a):"undefined"!=typeof module&&module&&module.exports?a(angular,require("moment")):a(angular,window.moment)}(); +"format amd";!function(){"use strict";function a(a){return angular.isUndefined(a)||null===a}function b(){try{return require("moment")}catch(a){throw new Error("Please install moment via npm. Please reference to: https://github.com/urish/angular-moment")}}function c(c,d){if("undefined"==typeof d){if("function"!=typeof require)throw new Error("Moment cannot be found by angular-moment! Please reference to: https://github.com/urish/angular-moment");d=b()}return c.module("angularMoment",[]).constant("angularMomentConfig",{preprocess:null,timezone:null,format:null,statefulFilters:!0}).constant("moment",d).constant("amTimeAgoConfig",{withoutSuffix:!1,serverTime:null,titleFormat:null,fullDateThreshold:null,fullDateFormat:null}).directive("amTimeAgo",["$window","moment","amMoment","amTimeAgoConfig",function(b,d,e,f){return function(g,h,i){function j(){var a;if(p)a=p;else if(f.serverTime){var b=(new Date).getTime(),c=b-v+f.serverTime;a=d(c)}else a=d();return a}function k(){q&&(b.clearTimeout(q),q=null)}function l(a){var c=j().diff(a,"day"),d=t&&c>=t;if(d?h.text(a.format(u)):h.text(a.from(j(),r)),s&&y&&h.attr("title",a.local().format(s)),!d){var e=Math.abs(j().diff(a,"minute")),f=3600;1>e?f=1:60>e?f=30:180>e&&(f=300),q=b.setTimeout(function(){l(a)},1e3*f)}}function m(a){x&&h.attr("datetime",a)}function n(){if(k(),o){var a=e.preprocessDate(o);l(a),m(a.toISOString())}}var o,p,q=null,r=f.withoutSuffix,s=f.titleFormat,t=f.fullDateThreshold,u=f.fullDateFormat,v=(new Date).getTime(),w=i.amTimeAgo,x="TIME"===h[0].nodeName.toUpperCase(),y=!h.attr("title");g.$watch(w,function(b){return a(b)||""===b?(k(),void(o&&(h.text(""),m(""),o=null))):(o=b,void n())}),c.isDefined(i.amFrom)&&g.$watch(i.amFrom,function(b){p=a(b)||""===b?null:d(b),n()}),c.isDefined(i.amWithoutSuffix)&&g.$watch(i.amWithoutSuffix,function(a){"boolean"==typeof a?(r=a,n()):r=f.withoutSuffix}),i.$observe("amFullDateThreshold",function(a){t=a,n()}),i.$observe("amFullDateFormat",function(a){u=a,n()}),g.$on("$destroy",function(){k()}),g.$on("amMoment:localeChanged",function(){n()})}}]).service("amMoment",["moment","$rootScope","$log","angularMomentConfig",function(a,b,d,e){var f=null;this.changeLocale=function(d,e){var f=a.locale(d,e);return c.isDefined(d)&&b.$broadcast("amMoment:localeChanged"),f},this.changeTimezone=function(c){a.tz&&a.tz.setDefault?(a.tz.setDefault(c),b.$broadcast("amMoment:timezoneChanged")):d.warn("angular-moment: changeTimezone() works only with moment-timezone.js v0.3.0 or greater."),e.timezone=c,f=c},this.preprocessDate=function(b){return f!==e.timezone&&this.changeTimezone(e.timezone),e.preprocess?e.preprocess(b):a(!isNaN(parseFloat(b))&&isFinite(b)?parseInt(b,10):b)}}]).filter("amParse",["moment",function(a){return function(b,c){return a(b,c)}}]).filter("amFromUnix",["moment",function(a){return function(b){return a.unix(b)}}]).filter("amUtc",["moment",function(a){return function(b){return a.utc(b)}}]).filter("amUtcOffset",["amMoment",function(a){function b(b,c){return a.preprocessDate(b).utcOffset(c)}return b}]).filter("amLocal",["moment",function(a){return function(b){return a.isMoment(b)?b.local():null}}]).filter("amTimezone",["amMoment","angularMomentConfig","$log",function(a,b,c){function d(b,d){var e=a.preprocessDate(b);return d?e.tz?e.tz(d):(c.warn("angular-moment: named timezone specified but moment.tz() is undefined. Did you forget to include moment-timezone.js ?"),e):e}return d}]).filter("amCalendar",["moment","amMoment","angularMomentConfig",function(b,c,d){function e(b){if(a(b))return"";var d=c.preprocessDate(b);return d.isValid()?d.calendar():""}return e.$stateful=d.statefulFilters,e}]).filter("amDifference",["moment","amMoment","angularMomentConfig",function(b,c,d){function e(d,e,f,g){if(a(d))return"";var h=c.preprocessDate(d),i=a(e)?b():c.preprocessDate(e);return h.isValid()&&i.isValid()?h.diff(i,f,g):""}return e.$stateful=d.statefulFilters,e}]).filter("amDateFormat",["moment","amMoment","angularMomentConfig",function(b,c,d){function e(b,d){if(a(b))return"";var e=c.preprocessDate(b);return e.isValid()?e.format(d):""}return e.$stateful=d.statefulFilters,e}]).filter("amDurationFormat",["moment","angularMomentConfig",function(b,c){function d(c,d,e){return a(c)?"":b.duration(c,d).humanize(e)}return d.$stateful=c.statefulFilters,d}]).filter("amTimeAgo",["moment","amMoment","angularMomentConfig",function(b,c,d){function e(d,e,f){var g,h;return a(d)?"":(d=c.preprocessDate(d),g=b(d),g.isValid()?(h=b(f),!a(f)&&h.isValid()?g.from(h,e):g.fromNow(e)):"")}return e.$stateful=d.statefulFilters,e}]).filter("amSubtract",["moment","angularMomentConfig",function(b,c){function d(c,d,e){return a(c)?"":b(c).subtract(parseInt(d,10),e)}return d.$stateful=c.statefulFilters,d}]).filter("amAdd",["moment","angularMomentConfig",function(b,c){function d(c,d,e){return a(c)?"":b(c).add(parseInt(d,10),e)}return d.$stateful=c.statefulFilters,d}]).filter("amStartOf",["moment","angularMomentConfig",function(b,c){function d(c,d){return a(c)?"":b(c).startOf(d)}return d.$stateful=c.statefulFilters,d}]).filter("amEndOf",["moment","angularMomentConfig",function(b,c){function d(c,d){return a(c)?"":b(c).endOf(d)}return d.$stateful=c.statefulFilters,d}])}"function"==typeof define&&define.amd?define(["angular","moment"],c):"undefined"!=typeof module&&module&&module.exports?(c(require("angular"),require("moment")),module.exports="angularMoment"):c(angular,("undefined"!=typeof global?global:window).moment)}(); //# sourceMappingURL=angular-moment.min.js.map \ No newline at end of file diff --git a/static/partials/build-view.html b/static/partials/build-view.html index 43f48a7ce..d7e934936 100644 --- a/static/partials/build-view.html +++ b/static/partials/build-view.html @@ -19,8 +19,14 @@
+ +
+ {{ originalBuild.error }} +
+ -
+
@@ -57,7 +63,8 @@
+ build-updated="setUpdatedBuild(build)" + ng-show="!originalBuild.error">