Merge pull request #2718 from coreos-inc/tag-expiration
Formal tag expiration support
This commit is contained in:
commit
a6db05e8b5
32 changed files with 621 additions and 117 deletions
|
@ -20,30 +20,10 @@
|
|||
color: #777;
|
||||
}
|
||||
|
||||
.service-keys-manager-element .expired a {
|
||||
color: #D64456;
|
||||
}
|
||||
|
||||
.service-keys-manager-element .critical a {
|
||||
color: #F77454;
|
||||
}
|
||||
|
||||
.service-keys-manager-element .warning a {
|
||||
color: #FCA657;
|
||||
}
|
||||
|
||||
.service-keys-manager-element .info a {
|
||||
color: #2FC98E;
|
||||
}
|
||||
|
||||
.service-keys-manager-element .rotation {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.service-keys-manager-element .no-expiration {
|
||||
color: #128E72;
|
||||
}
|
||||
|
||||
.service-keys-manager-element .approval-automatic {
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<span class="datetime-picker-element">
|
||||
<input class="form-control" type="text" ng-model="entered_datetime"/>
|
||||
<input class="form-control" type="text" ng-model="selected_datetime"/>
|
||||
</span>
|
|
@ -57,6 +57,12 @@
|
|||
<i class="fa fa-times"></i><span class="text">Delete Tags</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-if="repository.can_write">
|
||||
<a ng-click="askChangeTagsExpiration(checkedTags.checked)"
|
||||
ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''">
|
||||
<i class="fa fa-clock-o"></i><span class="text">Change Tags Expiration</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</span>
|
||||
|
@ -105,6 +111,11 @@
|
|||
style="width: 80px;">
|
||||
<a ng-click="orderBy('size')" data-title="The compressed size of the tag's image" data-container="body" bs-tooltip>Size</a>
|
||||
</td>
|
||||
<td class="hidden-sm hidden-xs"
|
||||
ng-class="tablePredicateClass('expiration_date', options.predicate, options.reverse)"
|
||||
style="width: 140px;">
|
||||
<a ng-click="orderBy('expiration_date')" data-title="When the tag expires" data-container="body" bs-tooltip>Expires</a>
|
||||
</td>
|
||||
<td class="hidden-xs hidden-sm"
|
||||
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
||||
style="width: 140px;">
|
||||
|
@ -133,12 +144,16 @@
|
|||
ng-if="repository.trust_enabled">
|
||||
<tag-signing-display tag="tag" delegations="repoDelegationsInfo" compact="true"></tag-signing-display>
|
||||
</td>
|
||||
|
||||
<!-- Last Modified -->
|
||||
<td class="hidden-xs">
|
||||
<span bo-if="tag.last_modified" data-title="{{ tag.last_modified | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip>
|
||||
<span am-time-ago="tag.last_modified"></span>
|
||||
</span>
|
||||
<span bo-if="!tag.last_modified">Unknown</span>
|
||||
</td>
|
||||
|
||||
<!-- Security scanning -->
|
||||
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-xs">
|
||||
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
|
||||
<span class="vuln-load-error" ng-if="getTagVulnerabilities(tag).hasError"
|
||||
|
@ -212,7 +227,20 @@
|
|||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Size -->
|
||||
<td class="hidden-sm hidden-xs" bo-text="tag.size | bytes"></td>
|
||||
|
||||
<!-- Expiration -->
|
||||
<td class="hidden-xs hidden-sm">
|
||||
<a ng-click="askChangeTagsExpiration([tag])"
|
||||
ng-if="!repository.tag_operations_disabled && repository.can_write">
|
||||
<expiration-status-view expiration-date="tag.expiration_date"></expiration-status-view>
|
||||
</a>
|
||||
<expiration-status-view expiration-date="tag.expiration_date" ng-if="repository.tag_operations_disabled || !repository.can_write"></expiration-status-view>
|
||||
</td>
|
||||
|
||||
<!-- Image link -->
|
||||
<td class="hidden-xs hidden-sm image-id-col">
|
||||
<span class="image-link" repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></span>
|
||||
</td>
|
||||
|
@ -254,6 +282,10 @@
|
|||
ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''">
|
||||
<i class="fa fa-times"></i> Delete Tag
|
||||
</span>
|
||||
<span class="cor-option" option-click="askChangeTagsExpiration([tag])"
|
||||
ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''">
|
||||
<i class="fa fa-clock-o"></i> Change Expiration
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
@ -261,7 +293,7 @@
|
|||
</tr>
|
||||
<tr ng-if="expandedView">
|
||||
<td class="checkbox-col"></td>
|
||||
<td class="labels-col" colspan="{{5 + (Features.SECURITY_SCANNER ? 1 : 0) + (repository.trust_enabled ? 1 : 0) }}">
|
||||
<td class="labels-col" colspan="{{6 + (Features.SECURITY_SCANNER ? 1 : 0) + (repository.trust_enabled ? 1 : 0) }}">
|
||||
<!-- Labels -->
|
||||
<div class="manifest-label-list" repository="repository"
|
||||
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
|
||||
|
|
|
@ -113,20 +113,14 @@
|
|||
<span am-time-ago="key.created_date"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="rotation" bo-if="key.expiration_date && getExpirationInfo(key).willRotate">
|
||||
<span class="rotation" bo-if="key.expiration_date && willRotate(key)">
|
||||
<i class="fa" ng-class="getExpirationInfo(key).icon"></i>
|
||||
Automatically rotated <span am-time-ago="getRotationDate(key)"></span>
|
||||
</span>
|
||||
<span bo-if="key.expiration_date && !getExpirationInfo(key).willRotate">
|
||||
<span ng-class="getExpirationInfo(key).className">
|
||||
<a ng-click="showChangeExpiration(key)">
|
||||
<i class="fa" ng-class="getExpirationInfo(key).icon"></i>
|
||||
Expire<span bo-if="getExpirationInfo(key).className != 'expired'">s</span><span bo-if="getExpirationInfo(key).className == 'expired'">d</span> <span am-time-ago="key.expiration_date"></span>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
<span class="no-expiration" bo-if="!key.expiration_date">
|
||||
<i class="fa fa-check"></i> Does not expire
|
||||
<span bo-if="!willRotate(key)">
|
||||
<a ng-click="showChangeExpiration(key)">
|
||||
<expiration-status-view expiration-date="key.expiration_date"></expiration-status-view>
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -111,6 +111,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Tags Expiration -->
|
||||
<div class="cor-confirm-dialog"
|
||||
dialog-context="changeTagsExpirationInfo"
|
||||
dialog-action="changeTagsExpiration(info.tags, info.expiration_date, callback)"
|
||||
dialog-title="Change Tags Expiration"
|
||||
dialog-action-title="Change Expiration">
|
||||
<form class="expiration-form">
|
||||
<label>Tags that will be updated:</label>
|
||||
<ul class="delete-tag-list">
|
||||
<li ng-repeat="tag_info in changeTagsExpirationInfo.tags">
|
||||
<span class="label label-default tag">{{ tag_info.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<label style="margin-top: 20px;">Expiration Date:</label>
|
||||
<span class="datetime-picker" datetime="changeTagsExpirationInfo.expiration_date"
|
||||
ng-if="changeTagsExpirationInfo"></span>
|
||||
<span class="co-help-text">
|
||||
If specified, the date and time that the key expires. If set to none, the tag(s) will not expire.
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Delete Tag Confirm -->
|
||||
<div class="cor-confirm-dialog"
|
||||
dialog-context="deleteTagInfo"
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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'};
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
Reference in a new issue