diff --git a/config.py b/config.py index 6a6c9c2e6..bedddcda7 100644 --- a/config.py +++ b/config.py @@ -194,4 +194,7 @@ class DefaultConfig(object): SYSTEM_SERVICES_PATH = "conf/init/" # Services that should not be shown in the logs view. - SYSTEM_SERVICE_BLACKLIST = [] \ No newline at end of file + SYSTEM_SERVICE_BLACKLIST = [] + + # Temporary tag expiration in seconds, this may actually be longer based on GC policy + PUSH_TEMP_TAG_EXPIRATION_S = 60 * 60 diff --git a/data/database.py b/data/database.py index d23157c0c..162057530 100644 --- a/data/database.py +++ b/data/database.py @@ -469,6 +469,7 @@ class RepositoryTag(BaseModel): repository = ForeignKeyField(Repository) lifetime_start_ts = IntegerField(default=_get_epoch_timestamp) lifetime_end_ts = IntegerField(null=True, index=True) + hidden = BooleanField(default=False) class Meta: database = db diff --git a/data/migrations/versions/4ef04c61fcf9_allow_tags_to_be_marked_as_hidden.py b/data/migrations/versions/4ef04c61fcf9_allow_tags_to_be_marked_as_hidden.py new file mode 100644 index 000000000..e4fc1ea5e --- /dev/null +++ b/data/migrations/versions/4ef04c61fcf9_allow_tags_to_be_marked_as_hidden.py @@ -0,0 +1,26 @@ +"""Allow tags to be marked as hidden. + +Revision ID: 4ef04c61fcf9 +Revises: 509d2857566f +Create Date: 2015-02-18 16:34:16.586129 + +""" + +# revision identifiers, used by Alembic. +revision = '4ef04c61fcf9' +down_revision = '509d2857566f' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('repositorytag', sa.Column('hidden', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false())) + ### end Alembic commands ### + + +def downgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('repositorytag', 'hidden') + ### end Alembic commands ### diff --git a/data/model/legacy.py b/data/model/legacy.py index f04283c5a..331bf2720 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -5,6 +5,7 @@ import json import time from datetime import datetime, timedelta, date +from uuid import uuid4 from data.database import (User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility, RepositoryTag, EmailConfirmation, FederatedLogin, @@ -1561,15 +1562,20 @@ def _tag_alive(query): (RepositoryTag.lifetime_end_ts > int(time.time()))) -def list_repository_tags(namespace_name, repository_name): - return _tag_alive(RepositoryTag - .select(RepositoryTag, Image) - .join(Repository) - .join(Namespace, on=(Repository.namespace_user == Namespace.id)) - .switch(RepositoryTag) - .join(Image) - .where(Repository.name == repository_name, - Namespace.username == namespace_name)) +def list_repository_tags(namespace_name, repository_name, include_hidden=False): + query = _tag_alive(RepositoryTag + .select(RepositoryTag, Image) + .join(Repository) + .join(Namespace, on=(Repository.namespace_user == Namespace.id)) + .switch(RepositoryTag) + .join(Image) + .where(Repository.name == repository_name, + Namespace.username == namespace_name)) + + if not include_hidden: + query = query.where(RepositoryTag.hidden == False) + + return query def _garbage_collect_tags(namespace_name, repository_name): @@ -1786,10 +1792,8 @@ def create_or_update_tag(namespace_name, repository_name, tag_name, # No tag that needs to be ended pass - tag = RepositoryTag.create(repository=repo, image=image, name=tag_name, - lifetime_start_ts=now_ts) - - return tag + return RepositoryTag.create(repository=repo, image=image, name=tag_name, + lifetime_start_ts=now_ts) def delete_tag(namespace_name, repository_name, tag_name): @@ -1812,6 +1816,17 @@ def delete_tag(namespace_name, repository_name, tag_name): found.save() +def create_temporary_hidden_tag(repo, image, expiration_s): + """ Create a tag with a defined timeline, that will not appear in the UI or CLI. Returns the name + of the temporary tag. """ + now_ts = int(time.time()) + expire_ts = now_ts + expiration_s + tag_name = str(uuid4()) + RepositoryTag.create(repository=repo, image=image, name=tag_name, lifetime_start_ts=now_ts, + lifetime_end_ts=expire_ts, hidden=True) + return tag_name + + def purge_all_repository_tags(namespace_name, repository_name): """ Immediately purge all repository tags without respecting the lifeline procedure """ try: diff --git a/endpoints/registry.py b/endpoints/registry.py index dc5069e22..8222212a7 100644 --- a/endpoints/registry.py +++ b/endpoints/registry.py @@ -467,6 +467,10 @@ def put_image_json(namespace, repository, image_id): repo_image = model.find_create_or_link_image(image_id, repo, username, {}, store.preferred_locations[0]) + # Create a temporary tag to prevent this image from getting garbage collected while the push + # is in progress. + model.create_temporary_hidden_tag(repo, repo_image, app.config['PUSH_TEMP_TAG_EXPIRATION_S']) + uuid = repo_image.storage.uuid if image_id != data['id']: diff --git a/test/data/test.db b/test/data/test.db index 0856c2f6a..4da80c978 100644 Binary files a/test/data/test.db and b/test/data/test.db differ