Merge pull request #2718 from coreos-inc/tag-expiration

Formal tag expiration support
This commit is contained in:
josephschorr 2017-07-19 17:48:11 -04:00 committed by GitHub
commit a6db05e8b5
32 changed files with 621 additions and 117 deletions

View file

@ -74,7 +74,8 @@ angular.module('quay').directive('repoPanelTags', function () {
var tagData = $scope.repository.tags[tag];
var tagInfo = $.extend(tagData, {
'name': tag,
'last_modified_datetime': TableService.getReversedTimestamp(tagData.last_modified)
'last_modified_datetime': TableService.getReversedTimestamp(tagData.last_modified),
'expiration_date': tagData.expiration ? TableService.getReversedTimestamp(tagData.expiration) : null,
});
allTags.push(tagInfo);
@ -355,6 +356,10 @@ angular.module('quay').directive('repoPanelTags', function () {
$scope.tagActionHandler.askDeleteMultipleTags(tags);
};
$scope.askChangeTagsExpiration = function(tags) {
$scope.tagActionHandler.askChangeTagsExpiration(tags);
};
$scope.askAddTag = function(tag) {
$scope.tagActionHandler.askAddTag(tag.image_id);
};

View file

@ -12,7 +12,7 @@ angular.module('quay').directive('datetimePicker', function () {
'datetime': '=datetime',
},
controller: function($scope, $element) {
$scope.entered_datetime = null;
var datetimeSet = false;
$(function() {
$element.find('input').datetimepicker({
@ -24,11 +24,15 @@ angular.module('quay').directive('datetimePicker', function () {
});
$element.find('input').on("dp.change", function (e) {
$scope.datetime = e.date ? e.date.unix() : null;
$scope.$apply(function() {
$scope.datetime = e.date ? e.date.unix() : null;
});
});
});
$scope.$watch('entered_datetime', function(value) {
$scope.$watch('selected_datetime', function(value) {
if (!datetimeSet) { return; }
if (!value) {
if ($scope.datetime) {
$scope.datetime = null;
@ -39,14 +43,16 @@ angular.module('quay').directive('datetimePicker', function () {
$scope.datetime = (new Date(value)).getTime()/1000;
});
$scope.$watch('datetime', function(value) {
if (!value) {
$scope.entered_datetime = null;
return;
}
$scope.$watch('datetime', function(value) {
if (!value) {
$scope.selected_datetime = null;
datetimeSet = true;
return;
}
$scope.entered_datetime = moment.unix(value).format('LLL');
});
$scope.selected_datetime = moment.unix(value).format('LLL');
datetimeSet = true;
});
}
};
return directiveDefinitionObject;

View file

@ -0,0 +1,23 @@
.expiration-status-view-element .expired, .expiration-status-view-element .expired a {
color: #D64456;
}
.expiration-status-view-element .critical, .expiration-status-view-element .critical a {
color: #F77454;
}
.expiration-status-view-element .warning, .expiration-status-view-element .warning a {
color: #FCA657;
}
.expiration-status-view-element .info, .expiration-status-view-element .info a {
color: #2FC98E;
}
.expiration-status-view-element .no-expiration, .expiration-status-view-element .no-expiration a {
color: #aaa;
}
.expiration-status-view-element .fa {
margin-right: 6px;
}

View file

@ -0,0 +1,10 @@
<span class="expiration-status-view-element">
<span ng-if="::$ctrl.expirationDate" ng-class="::$ctrl.getExpirationInfo($ctrl.expirationDate).className" data-title="{{ $ctrl.expirationDate | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip>
<i class="fa" ng-class="::$ctrl.getExpirationInfo($ctrl.expirationDate).icon"></i>
<span am-time-ago="$ctrl.expirationDate"></span>
</a>
</span>
<span class="no-expiration" ng-if="::!$ctrl.expirationDate">
Never
</span>
</span>

View file

@ -0,0 +1,40 @@
import { Input, Component, Inject } from 'ng-metadata/core';
import * as moment from "moment";
import './expiration-status-view.component.css';
type expirationInfo = {
className: string;
icon: string;
};
/**
* A component that displays expiration status.
*/
@Component({
selector: 'expiration-status-view',
templateUrl: '/static/js/directives/ui/expiration-status-view/expiration-status-view.component.html',
})
export class ExpirationStatusViewComponent {
@Input('<') public expirationDate: Date;
private getExpirationInfo(expirationDate): expirationInfo|null {
if (!expirationDate) {
return null;
}
const expiration = moment(expirationDate);
if (moment().isAfter(expiration)) {
return {'className': 'expired', 'icon': 'fa-warning'};
}
if (moment().add(1, 'week').isAfter(expiration)) {
return {'className': 'critical', 'icon': 'fa-warning'};
}
if (moment().add(1, 'month').isAfter(expiration)) {
return {'className': 'warning', 'icon': 'fa-warning'};
}
return {'className': 'info', 'icon': 'fa-clock-o'};
}
}

View file

@ -271,6 +271,18 @@ angular.module('quay').directive('logsView', function () {
'manifest_label_add': 'Label {key} added to manifest {manifest_digest} under repository {namespace}/{repo}',
'manifest_label_delete': 'Label {key} deleted from manifest {manifest_digest} under repository {namespace}/{repo}',
'change_tag_expiration': function(metadata) {
if (metadata.expiration_date && metadata.old_expiration_date) {
return 'Tag {tag} set to expire on {expiration_date} (previously {old_expiration_date})';
} else if (metadata.expiration_date) {
return 'Tag {tag} set to expire on {expiration_date}';
} else if (metadata.old_expiration_date) {
return 'Tag {tag} set to no longer expire (previously {old_expiration_date})';
} else {
return 'Tag {tag} set to no longer expire';
}
},
// Note: These are deprecated.
'add_repo_webhook': 'Add webhook in repository {repo}',
'delete_repo_webhook': 'Delete webhook in repository {repo}'
@ -332,6 +344,7 @@ angular.module('quay').directive('logsView', function () {
'take_ownership': 'Take Namespace Ownership',
'manifest_label_add': 'Add Manifest Label',
'manifest_label_delete': 'Delete Manifest Label',
'change_tag_expiration': 'Change tag expiration',
// Note: these are deprecated.
'add_repo_webhook': 'Add webhook',

View file

@ -80,32 +80,19 @@ angular.module('quay').directive('serviceKeysManager', function () {
return moment(key.created_date).add(key.rotation_duration, 's').format('LLL');
};
$scope.getExpirationInfo = function(key) {
$scope.willRotate = function(key) {
if (!key.expiration_date) {
return '';
return false;
}
if (key.rotation_duration) {
var rotate_date = moment(key.created_date).add(key.rotation_duration, 's')
if (moment().isBefore(rotate_date)) {
return {'className': 'rotation', 'icon': 'fa-refresh', 'willRotate': true};
return true;
}
}
expiration_date = moment(key.expiration_date);
if (moment().isAfter(expiration_date)) {
return {'className': 'expired', 'icon': 'fa-warning'};
}
if (moment().add(1, 'week').isAfter(expiration_date)) {
return {'className': 'critical', 'icon': 'fa-warning'};
}
if (moment().add(1, 'month').isAfter(expiration_date)) {
return {'className': 'warning', 'icon': 'fa-warning'};
}
return {'className': 'info', 'icon': 'fa-check'};
return false;
};
$scope.showChangeName = function(key) {

View file

@ -18,6 +18,7 @@ angular.module('quay').directive('tagOperationsDialog', function () {
},
controller: function($scope, $element, $timeout, ApiService) {
$scope.addingTag = false;
$scope.changeTagsExpirationInfo = null;
var markChanged = function(added, removed) {
// Reload the repository.
@ -81,13 +82,58 @@ angular.module('quay').directive('tagOperationsDialog', function () {
$element.find('#createOrMoveTagModal').modal('hide');
});
ApiService.changeTagImage(data, params).then(function(resp) {
ApiService.changeTag(data, params).then(function(resp) {
$element.find('#createOrMoveTagModal').modal('hide');
$scope.addingTag = false;
markChanged([tag], []);
}, errorHandler);
};
$scope.changeTagsExpiration = function(tags, expiration_date, callback) {
if (!$scope.repository.can_write) { return; }
var count = tags.length;
var perform = function(index) {
if (index >= count) {
callback(true);
markChanged(tags, []);
return;
}
var tag_info = tags[index];
if (!tag_info) { return; }
$scope.changeTagExpiration(tag_info.name, expiration_date, function(result) {
if (!result) {
callback(false);
return;
}
perform(index + 1);
}, true);
};
perform(0);
};
$scope.changeTagExpiration = function(tag, expiration_date, callback) {
if (!$scope.repository.can_write) { return; }
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'tag': tag
};
var data = {
'expiration': expiration_date
};
var errorHandler = ApiService.errorDisplay('Cannot change tag expiration', callback);
ApiService.changeTag(data, params).then(function() {
callback(true);
}, errorHandler);
};
$scope.deleteMultipleTags = function(tags, callback) {
if (!$scope.repository.can_write) { return; }
@ -296,6 +342,19 @@ angular.module('quay').directive('tagOperationsDialog', function () {
}, ApiService.errorDisplay('Could not load manifest labels'));
},
'askChangeTagsExpiration': function(tags) {
if ($scope.alertOnTagOpsDisabled()) {
return;
}
var expiration_date = null;
expiration_date = tags[0].expiration_date ? tags[0].expiration_date / 1000 : null;
$scope.changeTagsExpirationInfo = {
'tags': tags,
'expiration_date': expiration_date
};
},
'askRestoreTag': function(tag, image_id, opt_manifest_digest) {
if ($scope.alertOnTagOpsDisabled()) {
return;

View file

@ -31,6 +31,7 @@ import { MarkdownToolbarComponent } from './directives/ui/markdown/markdown-tool
import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-editor.component';
import { DockerfileCommandComponent } from './directives/ui/dockerfile-command/dockerfile-command.component';
import { ImageCommandComponent } from './directives/ui/image-command/image-command.component';
import { ExpirationStatusViewComponent } from './directives/ui/expiration-status-view/expiration-status-view.component';
import { BrowserPlatform, browserPlatform } from './constants/platform.constant';
import { ManageTriggerComponent } from './directives/ui/manage-trigger/manage-trigger.component';
import { ClipboardCopyDirective } from './directives/ui/clipboard-copy/clipboard-copy.directive';
@ -74,6 +75,7 @@ import * as Clipboard from 'clipboard';
ImageCommandComponent,
TypeaheadDirective,
ManageTriggerComponent,
ExpirationStatusViewComponent,
ClipboardCopyDirective,
TriggerDescriptionComponent,
],