Add ability to revert tags via time machine
This commit is contained in:
parent
2a77bd2c92
commit
f19d2f684e
16 changed files with 277 additions and 19 deletions
|
@ -497,6 +497,7 @@ class RepositoryTag(BaseModel):
|
||||||
lifetime_start_ts = IntegerField(default=get_epoch_timestamp)
|
lifetime_start_ts = IntegerField(default=get_epoch_timestamp)
|
||||||
lifetime_end_ts = IntegerField(null=True, index=True)
|
lifetime_end_ts = IntegerField(null=True, index=True)
|
||||||
hidden = BooleanField(default=False)
|
hidden = BooleanField(default=False)
|
||||||
|
reversion = BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Add revert_tag log entry kind
|
||||||
|
|
||||||
|
Revision ID: 1c3decf6b9c4
|
||||||
|
Revises: 4ce2169efd3b
|
||||||
|
Create Date: 2015-04-16 17:14:11.154856
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1c3decf6b9c4'
|
||||||
|
down_revision = '4ce2169efd3b'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
op.bulk_insert(tables.logentrykind,
|
||||||
|
[
|
||||||
|
{'id': 47, 'name':'revert_tag'},
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.execute(
|
||||||
|
(tables.logentrykind.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('revert_tag')))
|
||||||
|
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Add reversion column to the tags table
|
||||||
|
|
||||||
|
Revision ID: 4ce2169efd3b
|
||||||
|
Revises: 2b4dc0818a5e
|
||||||
|
Create Date: 2015-04-16 17:10:16.039835
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4ce2169efd3b'
|
||||||
|
down_revision = '2b4dc0818a5e'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('repositorytag', sa.Column('reversion', sa.Boolean(), nullable=False))
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('repositorytag', 'reversion')
|
||||||
|
### end Alembic commands ###
|
|
@ -1762,13 +1762,17 @@ def _tag_alive(query, now_ts=None):
|
||||||
(RepositoryTag.lifetime_end_ts > now_ts))
|
(RepositoryTag.lifetime_end_ts > now_ts))
|
||||||
|
|
||||||
|
|
||||||
def list_repository_tag_history(repository, limit=100):
|
def list_repository_tag_history(repository, limit=100, specific_tag=None):
|
||||||
query = (RepositoryTag
|
query = (RepositoryTag
|
||||||
.select(RepositoryTag, Image)
|
.select(RepositoryTag, Image)
|
||||||
.join(Image)
|
.join(Image)
|
||||||
.where(RepositoryTag.repository == repository)
|
.where(RepositoryTag.repository == repository)
|
||||||
.order_by(RepositoryTag.lifetime_start_ts.desc())
|
.order_by(RepositoryTag.lifetime_start_ts.desc())
|
||||||
.limit(limit))
|
.limit(limit))
|
||||||
|
|
||||||
|
if specific_tag:
|
||||||
|
query = query.where(RepositoryTag.name == specific_tag)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
@ -1990,7 +1994,7 @@ def get_parent_images(namespace_name, repository_name, image_obj):
|
||||||
|
|
||||||
|
|
||||||
def create_or_update_tag(namespace_name, repository_name, tag_name,
|
def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||||
tag_docker_image_id):
|
tag_docker_image_id, reversion=False):
|
||||||
try:
|
try:
|
||||||
repo = _get_repository(namespace_name, repository_name)
|
repo = _get_repository(namespace_name, repository_name)
|
||||||
except Repository.DoesNotExist:
|
except Repository.DoesNotExist:
|
||||||
|
@ -2015,7 +2019,7 @@ def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||||
raise DataModelException('Invalid image with id: %s' % tag_docker_image_id)
|
raise DataModelException('Invalid image with id: %s' % tag_docker_image_id)
|
||||||
|
|
||||||
return RepositoryTag.create(repository=repo, image=image, name=tag_name,
|
return RepositoryTag.create(repository=repo, image=image, name=tag_name,
|
||||||
lifetime_start_ts=now_ts)
|
lifetime_start_ts=now_ts, reversion=reversion)
|
||||||
|
|
||||||
def delete_tag(namespace_name, repository_name, tag_name):
|
def delete_tag(namespace_name, repository_name, tag_name):
|
||||||
now_ts = get_epoch_timestamp()
|
now_ts = get_epoch_timestamp()
|
||||||
|
@ -2823,3 +2827,14 @@ def repository_is_starred(user, repository):
|
||||||
return True
|
return True
|
||||||
except Star.DoesNotExist:
|
except Star.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def revert_tag(namespace_name, repository_name, tag_name, docker_image_id):
|
||||||
|
""" Reverts a tag to a specific image ID. """
|
||||||
|
image = get_image_by_id(namespace_name, repository_name, docker_image_id)
|
||||||
|
if image is None:
|
||||||
|
raise DataModelException('Cannot revert to unknown image')
|
||||||
|
|
||||||
|
return create_or_update_tag(namespace_name, repository_name, tag_name, docker_image_id,
|
||||||
|
reversion=True)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from flask import request, abort
|
||||||
|
|
||||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||||
RepositoryParamResource, log_action, NotFound, validate_json_request,
|
RepositoryParamResource, log_action, NotFound, validate_json_request,
|
||||||
path_param, format_date)
|
path_param, format_date, parse_args, query_param)
|
||||||
from endpoints.api.image import image_view
|
from endpoints.api.image import image_view
|
||||||
from data import model
|
from data import model
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
@ -17,8 +17,11 @@ class ListRepositoryTags(RepositoryParamResource):
|
||||||
""" Resource for listing repository tags. """
|
""" Resource for listing repository tags. """
|
||||||
|
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
|
@parse_args
|
||||||
|
@query_param('specificTag', 'Filters the tags to the specific tag.', type=str, default='')
|
||||||
|
@query_param('limit', 'Limit to the number of results to return. Max 100.', type=int, default=50)
|
||||||
@nickname('listRepoTags')
|
@nickname('listRepoTags')
|
||||||
def get(self, namespace, repository):
|
def get(self, args, namespace, repository):
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
if not repo:
|
if not repo:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
@ -27,6 +30,7 @@ class ListRepositoryTags(RepositoryParamResource):
|
||||||
tag_info = {
|
tag_info = {
|
||||||
'name': tag.name,
|
'name': tag.name,
|
||||||
'docker_image_id': tag.image.docker_image_id,
|
'docker_image_id': tag.image.docker_image_id,
|
||||||
|
'reversion': tag.reversion,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag.lifetime_start_ts > 0:
|
if tag.lifetime_start_ts > 0:
|
||||||
|
@ -37,7 +41,9 @@ class ListRepositoryTags(RepositoryParamResource):
|
||||||
|
|
||||||
return tag_info
|
return tag_info
|
||||||
|
|
||||||
tags = model.list_repository_tag_history(repo, limit=100)
|
specific_tag = args.get('specificTag') or None
|
||||||
|
limit = min(100, max(1, args.get('limit', 50)))
|
||||||
|
tags = model.list_repository_tag_history(repo, limit=limit, specific_tag=specific_tag)
|
||||||
return {'tags': [tag_view(tag) for tag in tags]}
|
return {'tags': [tag_view(tag) for tag in tags]}
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,3 +140,54 @@ class RepositoryTagImages(RepositoryParamResource):
|
||||||
return {
|
return {
|
||||||
'images': [image_view(image, image_map) for image in all_images]
|
'images': [image_view(image, image_map) for image in all_images]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<repopath:repository>/tag/<tag>/revert')
|
||||||
|
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||||
|
@path_param('tag', 'The name of the tag')
|
||||||
|
class RevertTag(RepositoryParamResource):
|
||||||
|
""" Resource for reverting a repository tag back to a previous image. """
|
||||||
|
schemas = {
|
||||||
|
'RevertTag': {
|
||||||
|
'id': 'RevertTag',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Reverts a tag to a specific image',
|
||||||
|
'required': [
|
||||||
|
'image',
|
||||||
|
],
|
||||||
|
'properties': {
|
||||||
|
'image': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Image identifier to which the tag should point',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_repo_write
|
||||||
|
@nickname('revertTag')
|
||||||
|
@validate_json_request('RevertTag')
|
||||||
|
def post(self, namespace, repository, tag):
|
||||||
|
""" Reverts a repository tag back to a previous image in the repository. """
|
||||||
|
try:
|
||||||
|
tag_image = model.get_tag_image(namespace, repository, tag)
|
||||||
|
except model.DataModelException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# Revert the tag back to the previous image.
|
||||||
|
image_id = request.get_json()['image']
|
||||||
|
model.revert_tag(namespace, repository, tag, image_id)
|
||||||
|
model.garbage_collect_repository(namespace, repository)
|
||||||
|
|
||||||
|
# Log the reversion.
|
||||||
|
username = get_authenticated_user().username
|
||||||
|
log_action('revert_tag', namespace,
|
||||||
|
{'username': username, 'repo': repository, 'tag': tag,
|
||||||
|
'image': image_id, 'original_image': tag_image.docker_image_id},
|
||||||
|
repo=model.get_repository(namespace, repository))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'image_id': image_id,
|
||||||
|
'original_image_id': tag_image.docker_image_id
|
||||||
|
}
|
||||||
|
|
|
@ -220,6 +220,7 @@ def initialize_database():
|
||||||
LogEntryKind.create(name='create_tag')
|
LogEntryKind.create(name='create_tag')
|
||||||
LogEntryKind.create(name='move_tag')
|
LogEntryKind.create(name='move_tag')
|
||||||
LogEntryKind.create(name='delete_tag')
|
LogEntryKind.create(name='delete_tag')
|
||||||
|
LogEntryKind.create(name='revert_tag')
|
||||||
LogEntryKind.create(name='add_repo_permission')
|
LogEntryKind.create(name='add_repo_permission')
|
||||||
LogEntryKind.create(name='change_repo_permission')
|
LogEntryKind.create(name='change_repo_permission')
|
||||||
LogEntryKind.create(name='delete_repo_permission')
|
LogEntryKind.create(name='delete_repo_permission')
|
||||||
|
|
|
@ -65,7 +65,22 @@
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-panel-tags-element .options-col .fa-download {
|
.repo-panel-tags-element .options-col .fa-download, .repo-panel-tags-element .options-col .fa-history {
|
||||||
color: #999;
|
color: #999;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .tag-image-history-item .image-id {
|
||||||
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .tag-image-history-item .image-apply-time {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 11px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-tags-element .tag-image-history-item .fa-circle-o {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
|
@ -41,14 +41,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry .history-date-break:before {
|
.repo-tag-history-element .history-entry .history-date-break:before {
|
||||||
content: "";
|
content: "\f073";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background: #ccc;
|
top: 1px;
|
||||||
top: 4px;
|
left: -9px;
|
||||||
left: -7px;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry .history-icon {
|
.repo-tag-history-element .history-entry .history-icon {
|
||||||
|
@ -79,11 +80,20 @@
|
||||||
font-family: FontAwesome;
|
font-family: FontAwesome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.revert .history-icon:before {
|
||||||
|
content: "\f0e2";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry.delete .history-icon:before {
|
.repo-tag-history-element .history-entry.delete .history-icon:before {
|
||||||
content: "\f014";
|
content: "\f014";
|
||||||
font-family: FontAwesome;
|
font-family: FontAwesome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-tag-history-element .history-entry.current.revert .history-icon {
|
||||||
|
background-color: #F0C577;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-tag-history-element .history-entry.current.move .history-icon {
|
.repo-tag-history-element .history-entry.current.move .history-icon {
|
||||||
background-color: #77BFF0;
|
background-color: #77BFF0;
|
||||||
}
|
}
|
||||||
|
|
4
static/css/directives/ui/tag-operations-dialog.css
Normal file
4
static/css/directives/ui/tag-operations-dialog.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.tag-operations-dialog .image-id {
|
||||||
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
|
@ -35,6 +35,12 @@
|
||||||
from image
|
from image
|
||||||
<span class="image-link" repository="repository" image-id="entry.old_docker_image_id"></span>
|
<span class="image-link" repository="repository" image-id="entry.old_docker_image_id"></span>
|
||||||
</span>
|
</span>
|
||||||
|
<span ng-switch-when="revert">
|
||||||
|
was reverted to image
|
||||||
|
<span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
||||||
|
from image
|
||||||
|
<span class="image-link" repository="repository" image-id="entry.old_docker_image_id"></span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-datetime">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
|
<div class="history-datetime">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
style="min-width: 120px;">
|
style="min-width: 120px;">
|
||||||
<a href="javascript:void(0)" ng-click="orderBy('image_id')">Image</a>
|
<a href="javascript:void(0)" ng-click="orderBy('image_id')">Image</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="options-col" ng-if="repository.can_write"></td>
|
||||||
<td class="options-col"></td>
|
<td class="options-col"></td>
|
||||||
<td class="options-col"></td>
|
<td class="options-col"></td>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -105,12 +106,30 @@
|
||||||
ng-click="fetchTagActionHandler.askFetchTag(tag)">
|
ng-click="fetchTagActionHandler.askFetchTag(tag)">
|
||||||
</i>
|
</i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="options-col" ng-if="repository.can_write">
|
||||||
|
<div class="dropdown" style="text-align: left;">
|
||||||
|
<i class="fa fa-history dropdown-toggle" data-toggle="dropdown" data-title="Tag History"
|
||||||
|
ng-click="loadTagHistory(tag)"
|
||||||
|
bs-tooltip></i>
|
||||||
|
<ul class="dropdown-menu pull-right">
|
||||||
|
<li ng-if="!tagHistory[tag.name]"><div class="cor-loader"></div></li>
|
||||||
|
<li class="tag-image-history-item" ng-repeat="entry in tagHistory[tag.name]">
|
||||||
|
<a href="javascript:void(0)" ng-click="askRevertTag(tag, entry.docker_image_id)">
|
||||||
|
<div class="image-id">
|
||||||
|
<i class="fa fa-circle-o"
|
||||||
|
ng-style="{'color': imageMap[entry.docker_image_id].color || '#eee'}"></i>
|
||||||
|
{{ entry.docker_image_id.substr(0, 12) }}
|
||||||
|
</div>
|
||||||
|
<div class="image-apply-time">
|
||||||
|
{{ entry.start_ts * 1000 | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="options-col">
|
<td class="options-col">
|
||||||
<span class="cor-options-menu" ng-if="repository.can_write">
|
<span class="cor-options-menu" ng-if="repository.can_write">
|
||||||
<span class="cor-option" option-click="showHistory(true, tag.name)"
|
|
||||||
ng-if="tag.last_modified">
|
|
||||||
<i class="fa fa-history"></i> View Tag History
|
|
||||||
</span>
|
|
||||||
<span class="cor-option" option-click="askDeleteTag(tag.name)">
|
<span class="cor-option" option-click="askDeleteTag(tag.name)">
|
||||||
<i class="fa fa-times"></i> Delete Tag
|
<i class="fa fa-times"></i> Delete Tag
|
||||||
</span>
|
</span>
|
||||||
|
@ -137,5 +156,4 @@
|
||||||
<div class="tag-operations-dialog" repository="repository" images="images"
|
<div class="tag-operations-dialog" repository="repository" images="images"
|
||||||
action-handler="tagActionHandler"></div>
|
action-handler="tagActionHandler"></div>
|
||||||
|
|
||||||
<div class="fetch-tag-dialog" repository="repository" action-handler="fetchTagActionHandler">
|
<div class="fetch-tag-dialog" repository="repository" action-handler="fetchTagActionHandler"></div>
|
||||||
</div>
|
|
|
@ -85,3 +85,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Recert Tag Confirm -->
|
||||||
|
<div class="cor-confirm-dialog"
|
||||||
|
dialog-context="revertTagInfo"
|
||||||
|
dialog-action="revertTag(info.tag, info.image_id, callback)"
|
||||||
|
dialog-title="Revert Tag"
|
||||||
|
dialog-action-title="Revert Tag">
|
||||||
|
|
||||||
|
<div class="co-alert co-alert-warning">
|
||||||
|
This will change the image to which the tag points.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Are you sure you want to revert tag
|
||||||
|
<span class="label label-default tag">{{ revertTagInfo.tag.name }}</span> to image
|
||||||
|
<span class="image-id">{{ revertTagInfo.image_id.substr(0, 12) }}?</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -24,6 +24,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.iterationState = {};
|
$scope.iterationState = {};
|
||||||
|
$scope.tagHistory = {};
|
||||||
$scope.tagActionHandler = null;
|
$scope.tagActionHandler = null;
|
||||||
$scope.showingHistory = false;
|
$scope.showingHistory = false;
|
||||||
|
|
||||||
|
@ -84,6 +85,9 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
'count': imageMap[image_id].length,
|
'count': imageMap[image_id].length,
|
||||||
'tags': imageMap[image_id]
|
'tags': imageMap[image_id]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
imageMap[image_id]['color'] = colors(index);
|
||||||
|
|
||||||
++index;
|
++index;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -224,6 +228,24 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
|
|
||||||
return names.join(',');
|
return names.join(',');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.loadTagHistory = function(tag) {
|
||||||
|
delete $scope.tagHistory[tag.name];
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'specificTag': tag.name,
|
||||||
|
'limit': 5
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.listRepoTags(null, params).then(function(resp) {
|
||||||
|
$scope.tagHistory[tag.name] = resp.tags;
|
||||||
|
}, ApiService.errorDisplay('Could not load tag history'));
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.askRevertTag = function(tag, image_id) {
|
||||||
|
$scope.tagActionHandler.askRevertTag(tag, image_id);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
|
|
@ -98,6 +98,7 @@ angular.module('quay').directive('logsView', function () {
|
||||||
return 'Remove permission for token {token} from repository {repo}';
|
return 'Remove permission for token {token} from repository {repo}';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'revert_tag': 'Tag {tag} reverted to image {image} from image {original_image}',
|
||||||
'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}',
|
'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}',
|
||||||
'create_tag': 'Tag {tag} created in repository {repo} on image {image} by user {username}',
|
'create_tag': 'Tag {tag} created in repository {repo} on image {image} by user {username}',
|
||||||
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {repo} by user {username}',
|
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {repo} by user {username}',
|
||||||
|
@ -213,6 +214,7 @@ angular.module('quay').directive('logsView', function () {
|
||||||
'delete_tag': 'Delete Tag',
|
'delete_tag': 'Delete Tag',
|
||||||
'create_tag': 'Create Tag',
|
'create_tag': 'Create Tag',
|
||||||
'move_tag': 'Move Tag',
|
'move_tag': 'Move Tag',
|
||||||
|
'revert_tag':' Revert Tag',
|
||||||
'org_create_team': 'Create team',
|
'org_create_team': 'Create team',
|
||||||
'org_delete_team': 'Delete team',
|
'org_delete_team': 'Delete team',
|
||||||
'org_add_team_member': 'Add team member',
|
'org_add_team_member': 'Add team member',
|
||||||
|
|
|
@ -56,6 +56,7 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
'action': action,
|
'action': action,
|
||||||
'start_ts': tag.start_ts,
|
'start_ts': tag.start_ts,
|
||||||
'end_ts': tag.end_ts,
|
'end_ts': tag.end_ts,
|
||||||
|
'reversion': tag.reversion,
|
||||||
'time': time * 1000, // JS expects ms, not s since epoch.
|
'time': time * 1000, // JS expects ms, not s since epoch.
|
||||||
'docker_image_id': opt_docker_id || dockerImageId,
|
'docker_image_id': opt_docker_id || dockerImageId,
|
||||||
'old_docker_image_id': opt_old_docker_id || ''
|
'old_docker_image_id': opt_old_docker_id || ''
|
||||||
|
@ -73,7 +74,8 @@ angular.module('quay').directive('repoTagHistory', function () {
|
||||||
var futureEntry = currentEntries.length > 0 ? currentEntries[currentEntries.length - 1] : {};
|
var futureEntry = currentEntries.length > 0 ? currentEntries[currentEntries.length - 1] : {};
|
||||||
if (futureEntry.start_ts == tag.end_ts) {
|
if (futureEntry.start_ts == tag.end_ts) {
|
||||||
removeEntry(futureEntry);
|
removeEntry(futureEntry);
|
||||||
addEntry('move', tag.end_ts, futureEntry.docker_image_id, dockerImageId);
|
addEntry(futureEntry.reversion ? 'revert': 'move', tag.end_ts,
|
||||||
|
futureEntry.docker_image_id, dockerImageId);
|
||||||
} else {
|
} else {
|
||||||
addEntry('delete', tag.end_ts)
|
addEntry('delete', tag.end_ts)
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,6 +121,25 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
}, errorHandler);
|
}, errorHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.revertTag = function(tag, image_id, callback) {
|
||||||
|
if (!$scope.repository.can_write) { return; }
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||||
|
'tag': tag.name
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'image': image_id
|
||||||
|
};
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Cannot revert tag', callback);
|
||||||
|
ApiService.revertTag(data, params).then(function() {
|
||||||
|
callback(true);
|
||||||
|
markChanged([], [tag]);
|
||||||
|
}, errorHandler);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.actionHandler = {
|
$scope.actionHandler = {
|
||||||
'askDeleteTag': function(tag) {
|
'askDeleteTag': function(tag) {
|
||||||
$scope.deleteTagInfo = {
|
$scope.deleteTagInfo = {
|
||||||
|
@ -140,6 +159,20 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
$scope.addingTag = false;
|
$scope.addingTag = false;
|
||||||
$scope.addTagForm.$setPristine();
|
$scope.addTagForm.$setPristine();
|
||||||
$element.find('#createOrMoveTagModal').modal('show');
|
$element.find('#createOrMoveTagModal').modal('show');
|
||||||
|
},
|
||||||
|
|
||||||
|
'askRevertTag': function(tag, image_id) {
|
||||||
|
if (tag.image_id == image_id) {
|
||||||
|
bootbox.alert('This is the current image for the tag');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.revertTagInfo = {
|
||||||
|
'tag': tag,
|
||||||
|
'image_id': image_id
|
||||||
|
};
|
||||||
|
|
||||||
|
$element.find('#revertTagModal').modal('show');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue