Add UI for viewing and changing the expiration of tags
This commit is contained in:
parent
977539bf08
commit
99d7fde8ee
13 changed files with 329 additions and 26 deletions
|
@ -0,0 +1,25 @@
|
||||||
|
"""Add change_tag_expiration log type
|
||||||
|
|
||||||
|
Revision ID: d8989249f8f6
|
||||||
|
Revises: dc4af11a5f90
|
||||||
|
Create Date: 2017-06-21 21:18:25.948689
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd8989249f8f6'
|
||||||
|
down_revision = 'dc4af11a5f90'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
op.bulk_insert(tables.logentrykind, [
|
||||||
|
{'name': 'change_tag_expiration'},
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
op.execute(tables
|
||||||
|
.logentrykind
|
||||||
|
.delete()
|
||||||
|
.where(tables.logentrykind.c.name == op.inline_literal('change_tag_expiration')))
|
|
@ -1,13 +1,17 @@
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from calendar import timegm
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from peewee import IntegrityError, JOIN_LEFT_OUTER, fn
|
from peewee import IntegrityError, JOIN_LEFT_OUTER, fn
|
||||||
from data.model import (image, db_transaction, DataModelException, _basequery,
|
from data.model import (image, db_transaction, DataModelException, _basequery,
|
||||||
InvalidManifestException, TagAlreadyCreatedException, StaleTagException)
|
InvalidManifestException, TagAlreadyCreatedException, StaleTagException,
|
||||||
|
config)
|
||||||
from data.database import (RepositoryTag, Repository, Image, ImageStorage, Namespace, TagManifest,
|
from data.database import (RepositoryTag, Repository, Image, ImageStorage, Namespace, TagManifest,
|
||||||
RepositoryNotification, Label, TagManifestLabel, get_epoch_timestamp,
|
RepositoryNotification, Label, TagManifestLabel, get_epoch_timestamp,
|
||||||
db_for_update)
|
db_for_update)
|
||||||
|
from util.timedeltastring import convert_to_timedelta
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -570,3 +574,41 @@ def _load_repo_manifests(namespace, repo_name):
|
||||||
.join(Repository)
|
.join(Repository)
|
||||||
.join(Namespace, on=(Namespace.id == Repository.namespace_user))
|
.join(Namespace, on=(Namespace.id == Repository.namespace_user))
|
||||||
.where(Repository.name == repo_name, Namespace.username == namespace))
|
.where(Repository.name == repo_name, Namespace.username == namespace))
|
||||||
|
|
||||||
|
|
||||||
|
def change_repository_tag_expiration(namespace_name, repo_name, tag_name, expiration_date):
|
||||||
|
""" Changes the expiration of the tag with the given name to the given expiration datetime. If
|
||||||
|
the expiration datetime is None, then the tag is marked as not expiring.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tag = get_active_tag(namespace_name, repo_name, tag_name)
|
||||||
|
return change_tag_expiration(tag, expiration_date)
|
||||||
|
except RepositoryTag.DoesNotExist:
|
||||||
|
return (None, False)
|
||||||
|
|
||||||
|
|
||||||
|
def change_tag_expiration(tag, expiration_date):
|
||||||
|
""" Changes the expiration of the given tag to the given expiration datetime. If
|
||||||
|
the expiration datetime is None, then the tag is marked as not expiring.
|
||||||
|
"""
|
||||||
|
end_ts = None
|
||||||
|
min_expire_sec = convert_to_timedelta(config.app_config.get('LABELED_EXPIRATION_MINIMUM', '1h'))
|
||||||
|
max_expire_sec = convert_to_timedelta(config.app_config.get('LABELED_EXPIRATION_MAXIMUM', '104w'))
|
||||||
|
|
||||||
|
if expiration_date is not None:
|
||||||
|
offset = timegm(expiration_date.utctimetuple()) - tag.lifetime_start_ts
|
||||||
|
offset = min(max(offset, min_expire_sec.total_seconds()), max_expire_sec.total_seconds())
|
||||||
|
end_ts = tag.lifetime_start_ts + offset
|
||||||
|
|
||||||
|
if end_ts == tag.lifetime_end_ts:
|
||||||
|
return (None, True)
|
||||||
|
|
||||||
|
# Note: We check not just the ID of the tag but also its lifetime_end_ts, to ensure that it has
|
||||||
|
# not changed while we were updatings it expiration.
|
||||||
|
result = (RepositoryTag
|
||||||
|
.update(lifetime_end_ts=end_ts)
|
||||||
|
.where(RepositoryTag.id == tag.id,
|
||||||
|
RepositoryTag.lifetime_end_ts == tag.lifetime_end_ts)
|
||||||
|
.execute())
|
||||||
|
|
||||||
|
return (tag.lifetime_end_ts, result > 0)
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from data.database import Image, RepositoryTag, ImageStorage, Repository
|
from data.database import Image, RepositoryTag, ImageStorage, Repository
|
||||||
from data.model.repository import create_repository
|
from data.model.repository import create_repository
|
||||||
from data.model.tag import (list_active_repo_tags, create_or_update_tag, delete_tag,
|
from data.model.tag import (list_active_repo_tags, create_or_update_tag, delete_tag,
|
||||||
get_matching_tags, _tag_alive, get_matching_tags_for_images)
|
get_matching_tags, _tag_alive, get_matching_tags_for_images,
|
||||||
|
change_tag_expiration, get_active_tag)
|
||||||
from data.model.image import find_create_or_link_image
|
from data.model.image import find_create_or_link_image
|
||||||
|
from util.timedeltastring import convert_to_timedelta
|
||||||
|
|
||||||
from test.fixtures import *
|
from test.fixtures import *
|
||||||
|
|
||||||
|
@ -177,3 +180,34 @@ def test_list_active_tags(initialized_db):
|
||||||
# "Move" foo by updating it and make sure we don't get duplicates.
|
# "Move" foo by updating it and make sure we don't get duplicates.
|
||||||
create_or_update_tag('devtable', 'somenewrepo', 'foo', image2.docker_image_id)
|
create_or_update_tag('devtable', 'somenewrepo', 'foo', image2.docker_image_id)
|
||||||
assert_tags(repository, 'foo', 'bar')
|
assert_tags(repository, 'foo', 'bar')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('expiration_offset, expected_offset', [
|
||||||
|
(None, None),
|
||||||
|
('0s', '1h'),
|
||||||
|
('30m', '1h'),
|
||||||
|
('2h', '2h'),
|
||||||
|
('2w', '2w'),
|
||||||
|
('200w', '104w'),
|
||||||
|
])
|
||||||
|
def test_change_tag_expiration(expiration_offset, expected_offset, initialized_db):
|
||||||
|
repository = create_repository('devtable', 'somenewrepo', None)
|
||||||
|
image1 = find_create_or_link_image('foobarimage1', repository, None, {}, 'local_us')
|
||||||
|
footag = create_or_update_tag('devtable', 'somenewrepo', 'foo', image1.docker_image_id)
|
||||||
|
|
||||||
|
expiration_date = None
|
||||||
|
if expiration_offset is not None:
|
||||||
|
expiration_date = datetime.utcnow() + convert_to_timedelta(expiration_offset)
|
||||||
|
|
||||||
|
assert change_tag_expiration(footag, expiration_date)
|
||||||
|
|
||||||
|
# Lookup the tag again.
|
||||||
|
footag_updated = get_active_tag('devtable', 'somenewrepo', 'foo')
|
||||||
|
|
||||||
|
if expected_offset is None:
|
||||||
|
assert footag_updated.lifetime_end_ts is None
|
||||||
|
else:
|
||||||
|
start_date = datetime.utcfromtimestamp(footag_updated.lifetime_start_ts)
|
||||||
|
end_date = datetime.utcfromtimestamp(footag_updated.lifetime_end_ts)
|
||||||
|
expected_end_date = start_date + convert_to_timedelta(expected_offset)
|
||||||
|
assert end_date == expected_end_date
|
||||||
|
|
|
@ -334,6 +334,10 @@ class Repository(RepositoryParamResource):
|
||||||
last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts))
|
last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts))
|
||||||
tag_info['last_modified'] = last_modified
|
tag_info['last_modified'] = last_modified
|
||||||
|
|
||||||
|
if tag.lifetime_end_ts:
|
||||||
|
expiration = format_date(datetime.fromtimestamp(tag.lifetime_end_ts))
|
||||||
|
tag_info['expiration'] = expiration
|
||||||
|
|
||||||
if tag.tagmanifest is not None:
|
if tag.tagmanifest is not None:
|
||||||
tag_info['manifest_digest'] = tag.tagmanifest.digest
|
tag_info['manifest_digest'] = tag.tagmanifest.digest
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" Manage the tags of a repository. """
|
""" Manage the tags of a repository. """
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from flask import request, abort
|
from flask import request, abort
|
||||||
|
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
@ -54,12 +55,15 @@ class RepositoryTag(RepositoryParamResource):
|
||||||
schemas = {
|
schemas = {
|
||||||
'MoveTag': {
|
'MoveTag': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'description': 'Description of to which image a new or existing tag should point',
|
'description': 'Makes changes to a specific tag',
|
||||||
'required': ['image',],
|
|
||||||
'properties': {
|
'properties': {
|
||||||
'image': {
|
'image': {
|
||||||
'type': 'string',
|
'type': ['string', 'null'],
|
||||||
'description': 'Image identifier to which the tag should point',
|
'description': '(If specified) Image identifier to which the tag should point',
|
||||||
|
},
|
||||||
|
'image': {
|
||||||
|
'type': ['number', 'null'],
|
||||||
|
'description': '(If specified) The expiration for the image',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -75,25 +79,52 @@ class RepositoryTag(RepositoryParamResource):
|
||||||
if not TAG_REGEX.match(tag):
|
if not TAG_REGEX.match(tag):
|
||||||
abort(400, TAG_ERROR)
|
abort(400, TAG_ERROR)
|
||||||
|
|
||||||
image_id = request.get_json()['image']
|
repo = model.get_repo(namespace, repository)
|
||||||
repo = model.get_repo(namespace, repository, image_id)
|
|
||||||
if not repo:
|
if not repo:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
original_image_id = model.get_repo_tag_image(repo, tag)
|
if 'expiration' in request.get_json():
|
||||||
model.create_or_update_tag(namespace, repository, tag, image_id)
|
expiration = request.get_json().get('expiration')
|
||||||
|
expiration_date = None
|
||||||
|
if expiration is not None:
|
||||||
|
try:
|
||||||
|
expiration_date = datetime.utcfromtimestamp(float(expiration))
|
||||||
|
except ValueError:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
username = get_authenticated_user().username
|
if expiration_date <= datetime.now():
|
||||||
log_action('move_tag' if original_image_id else 'create_tag', namespace, {
|
abort(400)
|
||||||
'username': username,
|
|
||||||
'repo': repository,
|
|
||||||
'tag': tag,
|
|
||||||
'namespace': namespace,
|
|
||||||
'image': image_id,
|
|
||||||
'original_image': original_image_id
|
|
||||||
}, repo_name=repository)
|
|
||||||
|
|
||||||
_generate_and_store_manifest(namespace, repository, tag)
|
existing_end_ts, ok = model.change_repository_tag_expiration(namespace, repository, tag,
|
||||||
|
expiration_date)
|
||||||
|
if ok:
|
||||||
|
if not (existing_end_ts is None and expiration_date is None):
|
||||||
|
log_action('change_tag_expiration', namespace, {
|
||||||
|
'username': get_authenticated_user().username,
|
||||||
|
'repo': repository,
|
||||||
|
'tag': tag,
|
||||||
|
'namespace': namespace,
|
||||||
|
'expiration_date': expiration_date,
|
||||||
|
'old_expiration_date': existing_end_ts
|
||||||
|
}, repo=repo)
|
||||||
|
else:
|
||||||
|
abort(400, 'Could not update tag expiration; Tag has probably changed')
|
||||||
|
|
||||||
|
if 'image' in request.get_json():
|
||||||
|
image_id = request.get_json()['image']
|
||||||
|
original_image_id = model.get_repo_tag_image(repo, tag)
|
||||||
|
model.create_or_update_tag(namespace, repository, tag, image_id)
|
||||||
|
|
||||||
|
username = get_authenticated_user().username
|
||||||
|
log_action('move_tag' if original_image_id else 'create_tag', namespace, {
|
||||||
|
'username': username,
|
||||||
|
'repo': repository,
|
||||||
|
'tag': tag,
|
||||||
|
'namespace': namespace,
|
||||||
|
'image': image_id,
|
||||||
|
'original_image': original_image_id
|
||||||
|
}, repo_name=repository)
|
||||||
|
_generate_and_store_manifest(namespace, repository, tag)
|
||||||
|
|
||||||
return 'Updated', 201
|
return 'Updated', 201
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,43 @@ def find_no_repo_tag_history():
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('expiration_time, expected_status', [
|
||||||
|
(None, 201),
|
||||||
|
('aksdjhasd', 400),
|
||||||
|
])
|
||||||
|
def test_change_tag_expiration_default(expiration_time, expected_status, client, app):
|
||||||
|
with client_with_identity('devtable', client) as cl:
|
||||||
|
params = {
|
||||||
|
'repository': 'devtable/simple',
|
||||||
|
'tag': 'latest',
|
||||||
|
}
|
||||||
|
|
||||||
|
request_body = {
|
||||||
|
'expiration': expiration_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
conduct_api_call(cl, RepositoryTag, 'put', params, request_body, expected_status)
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_tag_expiration(client, app):
|
||||||
|
with client_with_identity('devtable', client) as cl:
|
||||||
|
params = {
|
||||||
|
'repository': 'devtable/simple',
|
||||||
|
'tag': 'latest',
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = model.tag.get_active_tag('devtable', 'simple', 'latest')
|
||||||
|
updated_expiration = tag.lifetime_start_ts + 60*60*24
|
||||||
|
|
||||||
|
request_body = {
|
||||||
|
'expiration': updated_expiration,
|
||||||
|
}
|
||||||
|
|
||||||
|
conduct_api_call(cl, RepositoryTag, 'put', params, request_body, 201)
|
||||||
|
tag = model.tag.get_active_tag('devtable', 'simple', 'latest')
|
||||||
|
assert tag.lifetime_end_ts == updated_expiration
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('test_image,test_tag,expected_status', [
|
@pytest.mark.parametrize('test_image,test_tag,expected_status', [
|
||||||
('image1', '-INVALID-TAG-NAME', 400),
|
('image1', '-INVALID-TAG-NAME', 400),
|
||||||
('image1', '.INVALID-TAG-NAME', 400),
|
('image1', '.INVALID-TAG-NAME', 400),
|
||||||
|
|
|
@ -20,7 +20,7 @@ from auth.permissions import (AdministerOrganizationPermission, CreateRepository
|
||||||
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
UserAdminPermission, UserReadPermission, SuperUserPermission)
|
||||||
from data import model
|
from data import model
|
||||||
from data.billing import get_plan
|
from data.billing import get_plan
|
||||||
from data.database import Repository as RepositoryTable, UserPromptTypes
|
from data.database import Repository as RepositoryTable
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
log_action, internal_only, require_user_admin, parse_args,
|
log_action, internal_only, require_user_admin, parse_args,
|
||||||
query_param, require_scope, format_date, show_if,
|
query_param, require_scope, format_date, show_if,
|
||||||
|
|
|
@ -350,6 +350,8 @@ def initialize_database():
|
||||||
LogEntryKind.create(name='manifest_label_add')
|
LogEntryKind.create(name='manifest_label_add')
|
||||||
LogEntryKind.create(name='manifest_label_delete')
|
LogEntryKind.create(name='manifest_label_delete')
|
||||||
|
|
||||||
|
LogEntryKind.create(name='change_tag_expiration')
|
||||||
|
|
||||||
ImageStorageLocation.create(name='local_eu')
|
ImageStorageLocation.create(name='local_eu')
|
||||||
ImageStorageLocation.create(name='local_us')
|
ImageStorageLocation.create(name='local_us')
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,12 @@
|
||||||
<i class="fa fa-times"></i><span class="text">Delete Tags</span>
|
<i class="fa fa-times"></i><span class="text">Delete Tags</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@ -105,6 +111,11 @@
|
||||||
style="width: 80px;">
|
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>
|
<a ng-click="orderBy('size')" data-title="The compressed size of the tag's image" data-container="body" bs-tooltip>Size</a>
|
||||||
</td>
|
</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"
|
<td class="hidden-xs hidden-sm"
|
||||||
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('image_id', options.predicate, options.reverse)"
|
||||||
style="width: 140px;">
|
style="width: 140px;">
|
||||||
|
@ -133,12 +144,16 @@
|
||||||
ng-if="repository.trust_enabled">
|
ng-if="repository.trust_enabled">
|
||||||
<tag-signing-display tag="tag" delegations="repoDelegationsInfo" compact="true"></tag-signing-display>
|
<tag-signing-display tag="tag" delegations="repoDelegationsInfo" compact="true"></tag-signing-display>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Last Modified -->
|
||||||
<td class="hidden-xs">
|
<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 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 am-time-ago="tag.last_modified"></span>
|
||||||
</span>
|
</span>
|
||||||
<span bo-if="!tag.last_modified">Unknown</span>
|
<span bo-if="!tag.last_modified">Unknown</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Security scanning -->
|
||||||
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-xs">
|
<td quay-require="['SECURITY_SCANNER']" class="security-scan-col hidden-xs">
|
||||||
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
|
<span class="cor-loader-inline" ng-if="getTagVulnerabilities(tag).loading"></span>
|
||||||
<span class="vuln-load-error" ng-if="getTagVulnerabilities(tag).hasError"
|
<span class="vuln-load-error" ng-if="getTagVulnerabilities(tag).hasError"
|
||||||
|
@ -212,7 +227,20 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Size -->
|
||||||
<td class="hidden-sm hidden-xs" bo-text="tag.size | bytes"></td>
|
<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">
|
<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>
|
<span class="image-link" repository="repository" image-id="tag.image_id" manifest-digest="tag.manifest_digest"></span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -254,6 +282,10 @@
|
||||||
ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''">
|
ng-class="repository.tag_operations_disabled ? 'disabled-option' : ''">
|
||||||
<i class="fa fa-times"></i> Delete Tag
|
<i class="fa fa-times"></i> Delete Tag
|
||||||
</span>
|
</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>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -261,7 +293,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="expandedView">
|
<tr ng-if="expandedView">
|
||||||
<td class="checkbox-col"></td>
|
<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 -->
|
<!-- Labels -->
|
||||||
<div class="manifest-label-list" repository="repository"
|
<div class="manifest-label-list" repository="repository"
|
||||||
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
|
manifest-digest="tag.manifest_digest" cache="labelCache"></div>
|
||||||
|
|
|
@ -111,6 +111,28 @@
|
||||||
</div>
|
</div>
|
||||||
</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"></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 -->
|
<!-- Delete Tag Confirm -->
|
||||||
<div class="cor-confirm-dialog"
|
<div class="cor-confirm-dialog"
|
||||||
dialog-context="deleteTagInfo"
|
dialog-context="deleteTagInfo"
|
||||||
|
|
|
@ -74,7 +74,8 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
var tagData = $scope.repository.tags[tag];
|
var tagData = $scope.repository.tags[tag];
|
||||||
var tagInfo = $.extend(tagData, {
|
var tagInfo = $.extend(tagData, {
|
||||||
'name': tag,
|
'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);
|
allTags.push(tagInfo);
|
||||||
|
@ -355,6 +356,10 @@ angular.module('quay').directive('repoPanelTags', function () {
|
||||||
$scope.tagActionHandler.askDeleteMultipleTags(tags);
|
$scope.tagActionHandler.askDeleteMultipleTags(tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.askChangeTagsExpiration = function(tags) {
|
||||||
|
$scope.tagActionHandler.askChangeTagsExpiration(tags);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.askAddTag = function(tag) {
|
$scope.askAddTag = function(tag) {
|
||||||
$scope.tagActionHandler.askAddTag(tag.image_id);
|
$scope.tagActionHandler.askAddTag(tag.image_id);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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_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}',
|
'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.
|
// Note: These are deprecated.
|
||||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||||
'delete_repo_webhook': 'Delete 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',
|
'take_ownership': 'Take Namespace Ownership',
|
||||||
'manifest_label_add': 'Add Manifest Label',
|
'manifest_label_add': 'Add Manifest Label',
|
||||||
'manifest_label_delete': 'Delete Manifest Label',
|
'manifest_label_delete': 'Delete Manifest Label',
|
||||||
|
'change_tag_expiration': 'Change tag expiration',
|
||||||
|
|
||||||
// Note: these are deprecated.
|
// Note: these are deprecated.
|
||||||
'add_repo_webhook': 'Add webhook',
|
'add_repo_webhook': 'Add webhook',
|
||||||
|
|
|
@ -81,13 +81,58 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
$element.find('#createOrMoveTagModal').modal('hide');
|
$element.find('#createOrMoveTagModal').modal('hide');
|
||||||
});
|
});
|
||||||
|
|
||||||
ApiService.changeTagImage(data, params).then(function(resp) {
|
ApiService.changeTag(data, params).then(function(resp) {
|
||||||
$element.find('#createOrMoveTagModal').modal('hide');
|
$element.find('#createOrMoveTagModal').modal('hide');
|
||||||
$scope.addingTag = false;
|
$scope.addingTag = false;
|
||||||
markChanged([tag], []);
|
markChanged([tag], []);
|
||||||
}, errorHandler);
|
}, 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) {
|
$scope.deleteMultipleTags = function(tags, callback) {
|
||||||
if (!$scope.repository.can_write) { return; }
|
if (!$scope.repository.can_write) { return; }
|
||||||
|
|
||||||
|
@ -296,6 +341,17 @@ angular.module('quay').directive('tagOperationsDialog', function () {
|
||||||
}, ApiService.errorDisplay('Could not load manifest labels'));
|
}, ApiService.errorDisplay('Could not load manifest labels'));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'askChangeTagsExpiration': function(tags) {
|
||||||
|
if ($scope.alertOnTagOpsDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.changeTagsExpirationInfo ={
|
||||||
|
'tags': tags,
|
||||||
|
'expiration_date': null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
'askRestoreTag': function(tag, image_id, opt_manifest_digest) {
|
'askRestoreTag': function(tag, image_id, opt_manifest_digest) {
|
||||||
if ($scope.alertOnTagOpsDisabled()) {
|
if ($scope.alertOnTagOpsDisabled()) {
|
||||||
return;
|
return;
|
||||||
|
|
Reference in a new issue