1
0
mirror of https://github.com/quay/quay.git synced 2025-07-31 18:44:32 +03:00

feat: Add auto-prune policy at repository level (PROJQUAY-6354) (#2691)

* feat: Add support for auto pruning at repository level (PROJQUAY-6354)

* Add repositoryautoprunepolicy table to alembic migration script

* Add repository auto-prune policy endpoints

* Add UI for repository auto-pruning policies

* case: apply repo auto-prune policy when no namespace policy given

* case: both namespace and repo pruning policy are given

* Add tests for repository autoprune endpoint

* Add cypress test for repository auto-prune

* Add repo auto-prune policy clean-up for repository deletion

* Add repository auto pruning tables to quay db snapshot for cypress tests

* Address review comments

* Add more tests + fix CI + reformat files

* Address review comments #2

---------

Signed-off-by: harishsurf <hgovinda@redhat.com>
This commit is contained in:
Harish Govindarajulu
2024-02-27 15:02:57 +05:30
committed by GitHub
parent 29258ae0c7
commit 98811f5397
23 changed files with 2098 additions and 51 deletions

View File

@ -748,6 +748,7 @@ class User(BaseModel):
OrganizationRhSkus, OrganizationRhSkus,
NamespaceAutoPrunePolicy, NamespaceAutoPrunePolicy,
AutoPruneTaskStatus, AutoPruneTaskStatus,
RepositoryAutoPrunePolicy,
} }
| appr_classes | appr_classes
| v22_classes | v22_classes
@ -968,6 +969,7 @@ class Repository(BaseModel):
UploadedBlob, UploadedBlob,
QuotaNamespaceSize, QuotaNamespaceSize,
QuotaRepositorySize, QuotaRepositorySize,
RepositoryAutoPrunePolicy,
} }
| appr_classes | appr_classes
| v22_classes | v22_classes
@ -2021,6 +2023,13 @@ class AutoPruneTaskStatus(BaseModel):
status = TextField(null=True) status = TextField(null=True)
class RepositoryAutoPrunePolicy(BaseModel):
uuid = CharField(default=uuid_generator, max_length=36, index=True, null=False)
repository = ForeignKeyField(Repository, index=True, null=False)
namespace = QuayUserField(index=True, null=False)
policy = JSONField(null=False, default={})
# Defines a map from full-length index names to the legacy names used in our code # Defines a map from full-length index names to the legacy names used in our code
# to meet length restrictions. # to meet length restrictions.
LEGACY_INDEX_MAP = { LEGACY_INDEX_MAP = {

View File

@ -0,0 +1,78 @@
"""repository autoprune policy
Revision ID: b4da5b09c8df
Revises: 41d15c93c299
Create Date: 2024-02-05 10:47:32.172623
"""
# revision identifiers, used by Alembic.
revision = "b4da5b09c8df"
down_revision = "41d15c93c299"
import sqlalchemy as sa
def upgrade(op, tables, tester):
op.create_table(
"repositoryautoprunepolicy",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("uuid", sa.String(length=36), nullable=False),
sa.Column("repository_id", sa.Integer(), nullable=False),
sa.Column("namespace_id", sa.Integer(), nullable=False),
sa.Column("policy", sa.Text(), nullable=False),
sa.ForeignKeyConstraint(
["repository_id"],
["repository.id"],
name=op.f("fk_repositoryautoprunepolicy_repository_id_repository"),
),
sa.ForeignKeyConstraint(
["namespace_id"],
["user.id"],
name=op.f("fk_repositoryautoprunepolicy_namespace_id_user"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_repositoryautoprunepolicyid")),
)
op.create_index(
"repositoryautoprunepolicy_repository_id",
"repositoryautoprunepolicy",
["repository_id"],
unique=True,
)
op.create_index(
"repositoryautoprunepolicy_namespace_id",
"repositoryautoprunepolicy",
["namespace_id"],
unique=True,
)
op.create_index(
"repositoryautoprunepolicy_uuid",
"repositoryautoprunepolicy",
["uuid"],
unique=True,
)
op.bulk_insert(
tables.logentrykind,
[
{"name": "create_repository_autoprune_policy"},
{"name": "update_repository_autoprune_policy"},
{"name": "delete_repository_autoprune_policy"},
],
)
def downgrade(op, tables, tester):
op.drop_table("repositoryautoprunepolicy")
op.execute(
tables.logentrykind.delete().where(
tables.logentrykind.c.name
== op.inline_literal("create_repository_autoprune_policy") | tables.logentrykind.c.name
== op.inline_literal("update_repository_autoprune_policy") | tables.logentrykind.c.name
== op.inline_literal("delete_repository_autoprune_policy")
)
)

View File

@ -173,6 +173,26 @@ class InvalidNamespaceException(DataModelException):
pass pass
class RepositoryAutoPrunePolicyAlreadyExists(DataModelException):
pass
class RepositoryAutoPrunePolicyDoesNotExist(DataModelException):
pass
class InvalidRepositoryAutoPrunePolicy(DataModelException):
pass
class InvalidRepositoryAutoPruneMethod(DataModelException):
pass
class InvalidRepositoryException(DataModelException):
pass
class TooManyLoginAttemptsException(Exception): class TooManyLoginAttemptsException(Exception):
def __init__(self, message, retry_after): def __init__(self, message, retry_after):
super(TooManyLoginAttemptsException, self).__init__(message) super(TooManyLoginAttemptsException, self).__init__(message)

View File

@ -2,21 +2,21 @@ import json
import logging.config import logging.config
from enum import Enum from enum import Enum
from data.database import AutoPruneTaskStatus, DeletedNamespace from data.database import AutoPruneTaskStatus
from data.database import NamespaceAutoPrunePolicy as NamespaceAutoPrunePolicyTable from data.database import NamespaceAutoPrunePolicy as NamespaceAutoPrunePolicyTable
from data.database import ( from data.database import Repository
Repository, from data.database import RepositoryAutoPrunePolicy as RepositoryAutoPrunePolicyTable
RepositoryState, from data.database import RepositoryState, User, db_for_update, get_epoch_timestamp_ms
User,
db_for_update,
get_epoch_timestamp_ms,
)
from data.model import ( from data.model import (
InvalidNamespaceAutoPruneMethod, InvalidNamespaceAutoPruneMethod,
InvalidNamespaceAutoPrunePolicy, InvalidNamespaceAutoPrunePolicy,
InvalidNamespaceException, InvalidNamespaceException,
InvalidRepositoryAutoPrunePolicy,
InvalidRepositoryException,
NamespaceAutoPrunePolicyAlreadyExists, NamespaceAutoPrunePolicyAlreadyExists,
NamespaceAutoPrunePolicyDoesNotExist, NamespaceAutoPrunePolicyDoesNotExist,
RepositoryAutoPrunePolicyAlreadyExists,
RepositoryAutoPrunePolicyDoesNotExist,
db_transaction, db_transaction,
log, log,
modelutil, modelutil,
@ -53,6 +53,22 @@ class NamespaceAutoPrunePolicy:
return {"uuid": self.uuid, "method": self.method, "value": self.config.get("value")} return {"uuid": self.uuid, "method": self.method, "value": self.config.get("value")}
class RepositoryAutoPrunePolicy:
def __init__(self, db_row):
config = json.loads(db_row.policy)
self._db_row = db_row
self.uuid = db_row.uuid
self.method = config.get("method")
self.config = config
self.repository_id = db_row.repository_id
def get_row(self):
return self._db_row
def get_view(self):
return {"uuid": self.uuid, "method": self.method, "value": self.config.get("value")}
def valid_value(method, value): def valid_value(method, value):
""" """
Method for validating the value provided for the policy method. Method for validating the value provided for the policy method.
@ -88,6 +104,19 @@ def assert_valid_namespace_autoprune_policy(policy_config):
raise InvalidNamespaceAutoPrunePolicy("Invalid value given for method type") raise InvalidNamespaceAutoPrunePolicy("Invalid value given for method type")
def assert_valid_repository_autoprune_policy(policy_config):
"""
Asserts that the policy config is valid.
"""
try:
method = AutoPruneMethod(policy_config.get("method"))
except ValueError:
raise InvalidRepositoryAutoPrunePolicy("Invalid method provided")
if not valid_value(method, policy_config.get("value")):
raise InvalidRepositoryAutoPrunePolicy("Invalid value given for method type")
def get_namespace_autoprune_policies_by_orgname(orgname): def get_namespace_autoprune_policies_by_orgname(orgname):
""" """
Get the autopruning policies for the specified namespace. Get the autopruning policies for the specified namespace.
@ -102,6 +131,34 @@ def get_namespace_autoprune_policies_by_orgname(orgname):
return [NamespaceAutoPrunePolicy(row) for row in query] return [NamespaceAutoPrunePolicy(row) for row in query]
def get_repository_autoprune_policies_by_repo_name(orgname, repo_name):
"""
Get the autopruning policies for the specified repository.
"""
query = (
RepositoryAutoPrunePolicyTable.select(RepositoryAutoPrunePolicyTable)
.join(Repository)
.join(User)
.where(
User.username == orgname,
RepositoryAutoPrunePolicyTable.repository == Repository.id,
Repository.name == repo_name,
)
)
return [RepositoryAutoPrunePolicy(row) for row in query]
def get_repository_autoprune_policies_by_repo_id(repo_id):
"""
Get the autopruning policies for the specified repository.
"""
query = RepositoryAutoPrunePolicyTable.select().where(
RepositoryAutoPrunePolicyTable.repository == repo_id,
)
return [RepositoryAutoPrunePolicy(row) for row in query]
def get_namespace_autoprune_policies_by_id(namespace_id): def get_namespace_autoprune_policies_by_id(namespace_id):
""" """
Get the autopruning policies for the namespace by id. Get the autopruning policies for the namespace by id.
@ -112,6 +169,16 @@ def get_namespace_autoprune_policies_by_id(namespace_id):
return [NamespaceAutoPrunePolicy(row) for row in query] return [NamespaceAutoPrunePolicy(row) for row in query]
def get_repository_autoprune_policies_by_namespace_id(namespace_id):
"""
Get all repository autopruning policies for a namespace by id.
"""
query = RepositoryAutoPrunePolicyTable.select().where(
RepositoryAutoPrunePolicyTable.namespace == namespace_id,
)
return [RepositoryAutoPrunePolicy(row) for row in query]
def get_namespace_autoprune_policy(orgname, uuid): def get_namespace_autoprune_policy(orgname, uuid):
""" """
Get the specific autopruning policy for the specified namespace by uuid. Get the specific autopruning policy for the specified namespace by uuid.
@ -128,6 +195,22 @@ def get_namespace_autoprune_policy(orgname, uuid):
return None return None
def get_repository_autoprune_policy_by_uuid(repo_name, uuid):
"""
Get the specific autopruning policy for the specified repository by uuid.
"""
try:
row = (
RepositoryAutoPrunePolicyTable.select(RepositoryAutoPrunePolicyTable)
.join(Repository)
.where(Repository.name == repo_name, RepositoryAutoPrunePolicyTable.uuid == uuid)
.get()
)
return RepositoryAutoPrunePolicy(row)
except RepositoryAutoPrunePolicyTable.DoesNotExist:
return None
def create_namespace_autoprune_policy(orgname, policy_config, create_task=False): def create_namespace_autoprune_policy(orgname, policy_config, create_task=False):
""" """
Creates the namespace auto-prune policy. If create_task is True, then it will also create Creates the namespace auto-prune policy. If create_task is True, then it will also create
@ -156,6 +239,37 @@ def create_namespace_autoprune_policy(orgname, policy_config, create_task=False)
return new_policy return new_policy
def create_repository_autoprune_policy(orgname, repo_name, policy_config, create_task=False):
"""
Creates the repository auto-prune policy. If create_task is True, it will check if auto-prune task is not already present,
and only then it will create the auto-prune task. Deletion of the task will be handled by the autoprune worker.
"""
with db_transaction():
namespace = get_active_namespace_user_by_username(orgname)
namespace_id = namespace.id
repo = repository.get_repository(orgname, repo_name)
if repo is None:
raise InvalidRepositoryException("Repository does not exist: %s" % repo_name)
if repository_has_autoprune_policy(repo.id):
raise RepositoryAutoPrunePolicyAlreadyExists(
"Policy for this repository already exists, delete existing to create new policy"
)
assert_valid_repository_autoprune_policy(policy_config)
new_policy = RepositoryAutoPrunePolicyTable.create(
namespace=namespace_id, repository=repo.id, policy=json.dumps(policy_config)
)
if create_task and not namespace_has_autoprune_task(namespace_id):
create_autoprune_task(namespace_id)
return new_policy
def update_namespace_autoprune_policy(orgname, uuid, policy_config): def update_namespace_autoprune_policy(orgname, uuid, policy_config):
""" """
Updates the namespace auto-prune policy with the provided policy config Updates the namespace auto-prune policy with the provided policy config
@ -184,9 +298,41 @@ def update_namespace_autoprune_policy(orgname, uuid, policy_config):
return True return True
def update_repository_autoprune_policy(orgname, repo_name, uuid, policy_config):
"""
Updates the repository auto-prune policy with the provided policy config
for the specified uuid.
"""
namespace = get_active_namespace_user_by_username(orgname)
namespace_id = namespace.id
repo = repository.get_repository(orgname, repo_name)
if repo is None:
raise InvalidRepositoryException("Repository does not exist: %s" % repo_name)
policy = get_repository_autoprune_policy_by_uuid(repo_name, uuid)
if policy is None:
raise RepositoryAutoPrunePolicyDoesNotExist(
f"Policy not found for repository: {repo_name} with uuid: {uuid}"
)
assert_valid_repository_autoprune_policy(policy_config)
(
RepositoryAutoPrunePolicyTable.update(policy=json.dumps(policy_config))
.where(
RepositoryAutoPrunePolicyTable.uuid == uuid,
RepositoryAutoPrunePolicyTable.namespace == namespace_id,
)
.execute()
)
return True
def delete_namespace_autoprune_policy(orgname, uuid): def delete_namespace_autoprune_policy(orgname, uuid):
""" """
Deletes the policy specified by the uuid Deletes the namespace policy specified by the uuid
""" """
with db_transaction(): with db_transaction():
@ -217,6 +363,43 @@ def delete_namespace_autoprune_policy(orgname, uuid):
return True return True
def delete_repository_autoprune_policy(orgname, repo_name, uuid):
"""
Deletes the repository policy specified by the uuid
"""
with db_transaction():
try:
namespace_id = User.select().where(User.username == orgname).get().id
except User.DoesNotExist:
raise InvalidNamespaceException("Invalid namespace provided: %s" % (orgname))
repo = repository.get_repository(orgname, repo_name)
if repo is None:
raise InvalidRepositoryException("Repository does not exist: %s" % repo_name)
policy = get_repository_autoprune_policy_by_uuid(repo_name, uuid)
if policy is None:
raise RepositoryAutoPrunePolicyDoesNotExist(
f"Policy not found for repository: {repo_name} with uuid: {uuid}"
)
response = (
RepositoryAutoPrunePolicyTable.delete()
.where(
RepositoryAutoPrunePolicyTable.uuid == uuid,
RepositoryAutoPrunePolicyTable.namespace == namespace_id,
)
.execute()
)
if not response:
raise RepositoryAutoPrunePolicyTable.DoesNotExist(
f"Policy not found for repository: {repo_name} with uuid: {uuid}"
)
return True
def namespace_has_autoprune_policy(namespace_id): def namespace_has_autoprune_policy(namespace_id):
return ( return (
NamespaceAutoPrunePolicyTable.select(1) NamespaceAutoPrunePolicyTable.select(1)
@ -225,6 +408,14 @@ def namespace_has_autoprune_policy(namespace_id):
) )
def repository_has_autoprune_policy(repository_id):
return (
RepositoryAutoPrunePolicyTable.select(1)
.where(RepositoryAutoPrunePolicyTable.repository == repository_id)
.exists()
)
def namespace_has_autoprune_task(namespace_id): def namespace_has_autoprune_task(namespace_id):
return ( return (
AutoPruneTaskStatus.select(1).where(AutoPruneTaskStatus.namespace == namespace_id).exists() AutoPruneTaskStatus.select(1).where(AutoPruneTaskStatus.namespace == namespace_id).exists()
@ -260,6 +451,7 @@ def fetch_autoprune_task(task_run_interval_ms=60 * 60 * 1000):
AutoPruneTaskStatus.select(AutoPruneTaskStatus) AutoPruneTaskStatus.select(AutoPruneTaskStatus)
.where( .where(
AutoPruneTaskStatus.namespace.not_in( AutoPruneTaskStatus.namespace.not_in(
# this basically skips ns if user is not enabled
User.select(User.id).where( User.select(User.id).where(
User.enabled == False, User.id == AutoPruneTaskStatus.namespace User.enabled == False, User.id == AutoPruneTaskStatus.namespace
) )
@ -411,12 +603,18 @@ def execute_policy_on_repo(policy, repo_id, namespace_id, tag_page_limit=100):
policy_to_func_map[policy.method](repo, policy.config, namespace, tag_page_limit) policy_to_func_map[policy.method](repo, policy.config, namespace, tag_page_limit)
def execute_policies_for_repo(policies, repo, namespace_id, tag_page_limit=100): def execute_policies_for_repo(ns_policies, repo, namespace_id, tag_page_limit=100):
""" """
Executes the policies for the given repository. Executes both repository and namespace level policies for the given repository. The policies
are applied in a serial fashion and are run asynchronosly in the background.
""" """
for policy in policies: for ns_policy in ns_policies:
execute_policy_on_repo(policy, repo, namespace_id, tag_page_limit) repo_policies = get_repository_autoprune_policies_by_repo_id(repo.id)
# note: currently only one policy is configured per repo
for repo_policy in repo_policies:
execute_policy_on_repo(repo_policy, repo.id, namespace_id, tag_page_limit)
# execute associated namespace policy
execute_policy_on_repo(ns_policy, repo.id, namespace_id, tag_page_limit)
def get_paginated_repositories_for_namespace(namespace_id, page_token=None, page_size=50): def get_paginated_repositories_for_namespace(namespace_id, page_token=None, page_size=50):
@ -438,12 +636,28 @@ def get_paginated_repositories_for_namespace(namespace_id, page_token=None, page
) )
def execute_namespace_polices(policies, namespace_id, repository_page_limit=50, tag_page_limit=100): def get_repository_by_policy_repo_id(policy_repo_id):
try:
return (
Repository.select(Repository.name)
.where(
Repository.id == policy_repo_id,
Repository.state == RepositoryState.NORMAL,
)
.get()
)
except Repository.DoesNotExist:
return None
def execute_namespace_polices(
ns_policies, namespace_id, repository_page_limit=50, tag_page_limit=100
):
""" """
Executes the given policies for the repositories in the provided namespace. Executes the given policies for the repositories in the provided namespace.
""" """
if not policies: if not ns_policies:
return return
page_token = None page_token = None
@ -453,7 +667,7 @@ def execute_namespace_polices(policies, namespace_id, repository_page_limit=50,
) )
for repo in repos: for repo in repos:
execute_policies_for_repo(policies, repo, namespace_id, tag_page_limit) execute_policies_for_repo(ns_policies, repo, namespace_id, tag_page_limit)
if page_token is None: if page_token is None:
break break

View File

@ -21,6 +21,7 @@ from data.database import (
Repository, Repository,
RepositoryActionCount, RepositoryActionCount,
RepositoryAuthorizedEmail, RepositoryAuthorizedEmail,
RepositoryAutoPrunePolicy,
RepositoryBuild, RepositoryBuild,
RepositoryBuildTrigger, RepositoryBuildTrigger,
RepositoryNotification, RepositoryNotification,
@ -105,6 +106,8 @@ def purge_repository(repo, force=False):
ManifestSecurityStatus.select().where(ManifestSecurityStatus.repository == repo).count() ManifestSecurityStatus.select().where(ManifestSecurityStatus.repository == repo).count()
== 0 == 0
) )
# Delete auto-prune policy associated with the repository
RepositoryAutoPrunePolicy.delete().where(RepositoryAutoPrunePolicy.repository == repo).execute()
# Delete any repository build triggers, builds, and any other large-ish reference tables for # Delete any repository build triggers, builds, and any other large-ish reference tables for
# the repository. # the repository.

View File

@ -52,9 +52,9 @@ def test_load_security_information(indexed_v4, expected_status, initialized_db):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"next_token, expected_next_token, expected_error", "next_token, expected_next_token, expected_error",
[ [
(None, V4ScanToken(56), None), (None, V4ScanToken(58), None),
(V4ScanToken(None), V4ScanToken(56), AssertionError), (V4ScanToken(None), V4ScanToken(58), AssertionError),
(V4ScanToken(1), V4ScanToken(56), None), (V4ScanToken(1), V4ScanToken(58), None),
], ],
) )
def test_perform_indexing(next_token, expected_next_token, expected_error, initialized_db): def test_perform_indexing(next_token, expected_next_token, expected_error, initialized_db):

View File

@ -0,0 +1,169 @@
import json
import pytest
from data.model.autoprune import *
from data.model.organization import create_organization
from data.model.repository import create_repository, set_repository_state
from data.model.user import get_user
from test.fixtures import *
ORG1_NAME = "org1"
ORG2_NAME = "org2"
ORG3_NAME = "org3"
REPO1_NAME = "repo1"
REPO2_NAME = "repo2"
REPO3_NAME = "repo3"
class TestRepositoryAutoprune:
@pytest.fixture(autouse=True)
def setup(self, initialized_db):
user = get_user("devtable")
self.org1 = create_organization(ORG1_NAME, f"{ORG1_NAME}@devtable.com", user)
self.org2 = create_organization(ORG2_NAME, f"{ORG2_NAME}@devtable.com", user)
self.org3 = create_organization(ORG3_NAME, f"{ORG3_NAME}@devtable.com", user)
self.number_of_tags_policy = {"method": "number_of_tags", "value": 10}
self.repo1 = create_repository(ORG1_NAME, REPO1_NAME, None)
set_repository_state(self.repo1, RepositoryState.NORMAL)
self.repo2 = create_repository(ORG2_NAME, REPO2_NAME, None)
set_repository_state(self.repo2, RepositoryState.NORMAL)
self.repo3 = create_repository(ORG3_NAME, REPO3_NAME, None)
set_repository_state(self.repo3, RepositoryState.NORMAL)
self.repository_policy3 = create_repository_autoprune_policy(
ORG3_NAME, REPO3_NAME, self.number_of_tags_policy, create_task=False
)
def test_repo_policy_creation_without_task(self):
# policy based on tag count
new_repo_policy1 = create_repository_autoprune_policy(
ORG1_NAME, REPO1_NAME, self.number_of_tags_policy, create_task=False
)
assert new_repo_policy1.namespace.id == self.org1.id
assert new_repo_policy1.repository.id == self.repo1.id
assert json.loads(new_repo_policy1.policy) == self.number_of_tags_policy
# policy based on tag creation date
create_date_policy = {"method": "creation_date", "value": "7d"}
new_repo_policy2 = create_repository_autoprune_policy(
ORG2_NAME, REPO2_NAME, create_date_policy, create_task=False
)
assert new_repo_policy2.namespace.id == self.org2.id
assert new_repo_policy2.repository.id == self.repo2.id
assert json.loads(new_repo_policy2.policy) == create_date_policy
def test_repo_policy_creation_with_task(self):
new_repo_policy = create_repository_autoprune_policy(
ORG1_NAME, REPO1_NAME, self.number_of_tags_policy, create_task=True
)
assert new_repo_policy.namespace.id == self.org1.id
assert new_repo_policy.repository.id == self.repo1.id
assert namespace_has_autoprune_task(self.org1.id) is True
def test_repo_policy_creation_with_incorrect_repo_name(self):
with pytest.raises(InvalidRepositoryException) as excerror:
create_repository_autoprune_policy(
ORG1_NAME, "nonexistentrepo", self.number_of_tags_policy, create_task=True
)
assert str(excerror.value) == "Repository does not exist: nonexistentrepo"
def test_repo_policy_creation_for_repo_with_policy(self):
with pytest.raises(RepositoryAutoPrunePolicyAlreadyExists) as excerror:
create_repository_autoprune_policy(
ORG3_NAME, REPO3_NAME, self.number_of_tags_policy, create_task=True
)
assert (
str(excerror.value)
== "Policy for this repository already exists, delete existing to create new policy"
)
def test_get_repo_policies_by_reponame(self):
repo_policies = get_repository_autoprune_policies_by_repo_name(ORG3_NAME, REPO3_NAME)
assert len(repo_policies) == 1
assert repo_policies[0]._db_row.namespace_id == self.org3.id
assert repo_policies[0].repository_id == self.repo3.id
repo2_policies = get_repository_autoprune_policies_by_repo_name(ORG2_NAME, REPO2_NAME)
assert len(repo2_policies) == 0
def test_get_repo_policies_by_namespace_id(self):
repo_policies = get_repository_autoprune_policies_by_namespace_id(self.org3.id)
assert len(repo_policies) == 1
assert repo_policies[0]._db_row.namespace_id == self.org3.id
assert repo_policies[0].repository_id == self.repo3.id
repo2_policies = get_repository_autoprune_policies_by_namespace_id(self.org2.id)
assert len(repo2_policies) == 0
def test_get_repo_policies_by_repo_id(self):
repo_policies = get_repository_autoprune_policies_by_repo_id(self.repo3.id)
assert len(repo_policies) == 1
assert repo_policies[0]._db_row.namespace_id == self.org3.id
assert repo_policies[0].repository_id == self.repo3.id
repo2_policies = get_repository_autoprune_policies_by_repo_id(self.repo2.id)
assert len(repo2_policies) == 0
def test_update_repo_policy(self):
new_policy_config = {"method": "number_of_tags", "value": 100}
updated = update_repository_autoprune_policy(
ORG3_NAME, REPO3_NAME, self.repository_policy3.uuid, new_policy_config
)
assert updated is True
repo_policies = get_repository_autoprune_policies_by_repo_name(ORG3_NAME, REPO3_NAME)
assert repo_policies[0].config == new_policy_config
def test_incorrect_update_repo_policy(self):
# incorrect uuid
with pytest.raises(RepositoryAutoPrunePolicyDoesNotExist) as excerror:
update_repository_autoprune_policy(ORG3_NAME, REPO3_NAME, "random-uuid", {})
assert (
str(excerror.value)
== f"Policy not found for repository: {REPO3_NAME} with uuid: random-uuid"
)
# incorrect reponame
with pytest.raises(InvalidRepositoryException) as excerror:
update_repository_autoprune_policy(
ORG3_NAME, "nonexistentrepo", self.repository_policy3.uuid, {}
)
assert str(excerror.value) == "Repository does not exist: nonexistentrepo"
def test_delete_repo_policy(self):
deleted = delete_repository_autoprune_policy(
ORG3_NAME, REPO3_NAME, self.repository_policy3.uuid
)
assert deleted is True
def test_incorrect_delete_repo_policy(self):
# incorrect uuid
with pytest.raises(RepositoryAutoPrunePolicyDoesNotExist) as excerror:
delete_repository_autoprune_policy(ORG3_NAME, REPO3_NAME, "random-uuid")
assert (
str(excerror.value)
== f"Policy not found for repository: {REPO3_NAME} with uuid: random-uuid"
)
# incorrect reponame
with pytest.raises(InvalidRepositoryException) as excerror:
delete_repository_autoprune_policy(
ORG3_NAME, "nonexistentrepo", self.repository_policy3.uuid
)
assert str(excerror.value) == "Repository does not exist: nonexistentrepo"
def test_repository_policy(self):
policy_exists = repository_has_autoprune_policy(self.repo3.id)
assert policy_exists is True
repository_policy = get_repository_autoprune_policy_by_uuid(
self.repo3.name, self.repository_policy3.uuid
)
assert repository_policy.uuid == self.repository_policy3.uuid
assert repository_policy.repository_id == self.repo3.id
resp = get_repository_autoprune_policy_by_uuid("nonexistentrepo", "randome-uuid")
assert resp is None

View File

@ -5,15 +5,21 @@ from flask import request
import features import features
from auth import scopes from auth import scopes
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from auth.permissions import AdministerOrganizationPermission from auth.permissions import (
AdministerOrganizationPermission,
AdministerRepositoryPermission,
)
from data import model from data import model
from data.registry_model import registry_model
from endpoints.api import ( from endpoints.api import (
ApiResource, ApiResource,
RepositoryParamResource,
allow_if_superuser, allow_if_superuser,
log_action, log_action,
nickname, nickname,
path_param, path_param,
request_error, request_error,
require_repo_admin,
require_scope, require_scope,
require_user_admin, require_user_admin,
resource, resource,
@ -231,6 +237,233 @@ class OrgAutoPrunePolicy(ApiResource):
return {"uuid": policy_uuid}, 200 return {"uuid": policy_uuid}, 200
@resource("/v1/repository/<apirepopath:repository>/autoprunepolicy/")
@path_param("repository", "The full path of the repository. e.g. namespace/name")
@show_if(features.AUTO_PRUNE)
class RepositoryAutoPrunePolicies(RepositoryParamResource):
"""
Resource for listing and creating repository auto-prune policies
"""
schemas = {
"AutoPrunePolicyConfig": {
"type": "object",
"description": "The policy configuration that is to be applied to the repository",
"required": ["method", "value"],
"properties": {
"method": {
"type": "string",
"description": "The method to use for pruning tags (number_of_tags, creation_date)",
},
"value": {
"type": ["integer", "string"],
"description": "The value to use for the pruning method (number of tags e.g. 10, time delta e.g. 7d (7 days))",
},
},
},
}
@require_repo_admin(allow_for_superuser=True)
@nickname("listRepositoryAutoPrunePolicies")
def get(self, namespace, repository):
"""
Lists the auto-prune policies for the repository
"""
permission = AdministerRepositoryPermission(namespace, repository)
if not permission.can():
raise Unauthorized()
if registry_model.lookup_repository(namespace, repository) is None:
raise NotFound()
policies = model.autoprune.get_repository_autoprune_policies_by_repo_name(
namespace, repository
)
return {"policies": [policy.get_view() for policy in policies]}
@require_repo_admin(allow_for_superuser=True)
@validate_json_request("AutoPrunePolicyConfig")
@nickname("createRepositoryAutoPrunePolicy")
def post(self, namespace, repository):
"""
Creates an auto-prune policy for the repository
"""
permission = AdministerRepositoryPermission(namespace, repository)
if not permission.can():
raise Unauthorized()
if registry_model.lookup_repository(namespace, repository) is None:
raise NotFound()
app_data = request.get_json()
method = app_data.get("method", None)
value = app_data.get("value", None)
if method is None or value is None:
request_error(message="Missing the following parameters: method, value")
policy_config = {
"method": method,
"value": value,
}
try:
policy = model.autoprune.create_repository_autoprune_policy(
namespace, repository, policy_config, create_task=True
)
except model.InvalidNamespaceException:
raise NotFound()
except model.InvalidRepositoryException:
raise NotFound()
except model.InvalidRepositoryAutoPrunePolicy as ex:
request_error(ex)
except model.RepositoryAutoPrunePolicyAlreadyExists as ex:
request_error(ex)
log_action(
"create_repository_autoprune_policy",
namespace,
{
"method": policy_config["method"],
"value": policy_config["value"],
"namespace": namespace,
"repo": repository,
},
repo_name=repository,
)
return {"uuid": policy.uuid}, 201
@resource("/v1/repository/<apirepopath:repository>/autoprunepolicy/<policy_uuid>")
@path_param("repository", "The full path of the repository. e.g. namespace/name")
@path_param("policy_uuid", "The unique ID of the policy")
@show_if(features.AUTO_PRUNE)
class RepositoryAutoPrunePolicy(RepositoryParamResource):
"""
Resource for fetching, updating, and deleting repository specific auto-prune policies
"""
schemas = {
"AutoPrunePolicyConfig": {
"type": "object",
"description": "The policy configuration that is to be applied to the repository",
"required": ["method", "value"],
"properties": {
"method": {
"type": "string",
"description": "The method to use for pruning tags (number_of_tags, creation_date)",
},
"value": {
"type": ["integer", "string"],
"description": "The value to use for the pruning method (number of tags e.g. 10, time delta e.g. 7d (7 days))",
},
},
},
}
@require_repo_admin(allow_for_superuser=True)
@nickname("getRepositoryAutoPrunePolicy")
def get(self, namespace, repository, policy_uuid):
"""
Fetches the auto-prune policy for the repository
"""
permission = AdministerRepositoryPermission(namespace, repository)
if not permission.can():
raise Unauthorized()
policy = model.autoprune.get_repository_autoprune_policy_by_uuid(repository, policy_uuid)
if policy is None:
raise NotFound()
return policy.get_view()
@require_repo_admin(allow_for_superuser=True)
@validate_json_request("AutoPrunePolicyConfig")
@nickname("updateRepositoryAutoPrunePolicy")
def put(self, namespace, repository, policy_uuid):
"""
Updates the auto-prune policy for the repository
"""
permission = AdministerRepositoryPermission(namespace, repository)
if not permission.can():
raise Unauthorized()
app_data = request.get_json()
method = app_data.get("method", None)
value = app_data.get("value", None)
if method is None or value is None:
request_error(message="Missing the following parameters: method, value")
policy_config = {
"method": method,
"value": value,
}
try:
updated = model.autoprune.update_repository_autoprune_policy(
namespace, repository, policy_uuid, policy_config
)
if not updated:
request_error(message="could not update policy")
except model.InvalidNamespaceException:
raise NotFound()
except model.InvalidRepositoryException:
raise NotFound()
except model.InvalidRepositoryAutoPrunePolicy as ex:
request_error(ex)
except model.RepositoryAutoPrunePolicyDoesNotExist as ex:
raise NotFound()
log_action(
"update_repository_autoprune_policy",
namespace,
{
"method": policy_config["method"],
"value": policy_config["value"],
"namespace": namespace,
"repo": repository,
},
repo_name=repository,
)
return {"uuid": policy_uuid}, 204
@require_repo_admin(allow_for_superuser=True)
@nickname("deleteRepositoryAutoPrunePolicy")
def delete(self, namespace, repository, policy_uuid):
"""
Deletes the auto-prune policy for the repository
"""
permission = AdministerRepositoryPermission(namespace, repository)
if not permission.can():
raise Unauthorized()
try:
updated = model.autoprune.delete_repository_autoprune_policy(
namespace, repository, policy_uuid
)
if not updated:
raise InvalidRequest("could not delete policy")
except model.InvalidNamespaceException:
raise NotFound()
except model.InvalidRepositoryException as ex:
raise NotFound()
except model.RepositoryAutoPrunePolicyDoesNotExist as ex:
raise NotFound()
log_action(
"delete_repository_autoprune_policy",
namespace,
{"policy_uuid": policy_uuid, "namespace": namespace, "repo": repository},
repo_name=repository,
)
return {"uuid": policy_uuid}, 200
@resource("/v1/user/autoprunepolicy/") @resource("/v1/user/autoprunepolicy/")
@show_if(features.AUTO_PRUNE) @show_if(features.AUTO_PRUNE)
class UserAutoPrunePolicies(ApiResource): class UserAutoPrunePolicies(ApiResource):

View File

@ -7,6 +7,8 @@ from data.model.log import get_latest_logs_query, get_log_entry_kinds
from endpoints.api.policy import ( from endpoints.api.policy import (
OrgAutoPrunePolicies, OrgAutoPrunePolicies,
OrgAutoPrunePolicy, OrgAutoPrunePolicy,
RepositoryAutoPrunePolicies,
RepositoryAutoPrunePolicy,
UserAutoPrunePolicies, UserAutoPrunePolicies,
UserAutoPrunePolicy, UserAutoPrunePolicy,
) )
@ -345,3 +347,187 @@ def test_delete_user_policy_nonexistent_policy(initialized_db, app):
{"orgname": "devtable", "policy_uuid": "doesnotexist"}, {"orgname": "devtable", "policy_uuid": "doesnotexist"},
expected_code=404, expected_code=404,
) )
def test_get_repo_policies(initialized_db, app):
with client_with_identity("devtable", app) as cl:
params = {"repository": "devtable/simple"}
response = conduct_api_call(cl, RepositoryAutoPrunePolicies, "GET", params).json
assert len(response["policies"]) == 1
assert response["policies"][0]["method"] == "number_of_tags"
assert response["policies"][0]["value"] == 10
def test_create_repo_policy(initialized_db, app):
with client_with_identity("devtable", app) as cl:
params = {"repository": "testorgforautoprune/autoprunerepo"}
response = conduct_api_call(
cl,
RepositoryAutoPrunePolicies,
"POST",
params,
{"method": "creation_date", "value": "2w"},
201,
).json
assert response["uuid"] is not None
assert (
model.autoprune.get_repository_autoprune_policy_by_uuid(
"autoprunerepo", response["uuid"]
)
is not None
)
org = model.organization.get_organization("testorgforautoprune")
assert model.autoprune.namespace_has_autoprune_task(org.id)
# Check audit log was created
logs = list(get_latest_logs_query(namespace="testorgforautoprune"))
log_kinds = get_log_entry_kinds()
log = None
for l in logs:
if l.kind == log_kinds["create_repository_autoprune_policy"]:
log = l
break
assert log is not None
assert json.loads(log.metadata_json)["method"] == "creation_date"
assert json.loads(log.metadata_json)["value"] == "2w"
assert json.loads(log.metadata_json)["namespace"] == "testorgforautoprune"
def test_create_repo_policy_already_existing(initialized_db, app):
with client_with_identity("devtable", app) as cl:
params = {"repository": "devtable/simple"}
response = conduct_api_call(
cl,
RepositoryAutoPrunePolicies,
"POST",
params,
{"method": "creation_date", "value": "2w"},
expected_code=400,
).json
assert (
response["error_message"]
== "Policy for this repository already exists, delete existing to create new policy"
)
def test_create_repo_policy_nonexistent_method(initialized_db, app):
with client_with_identity("devtable", app) as cl:
params = {"repository": "testorgforautoprune/autoprunerepo"}
response = conduct_api_call(
cl,
RepositoryAutoPrunePolicies,
"POST",
params,
{"method": "doesnotexist", "value": "2w"},
expected_code=400,
).json
assert response["error_message"] == "Invalid method provided"
def test_get_repo_policy(initialized_db, app):
policies = model.autoprune.get_repository_autoprune_policies_by_repo_name("devtable", "simple")
assert len(policies) == 1
policy_uuid = policies[0].uuid
with client_with_identity("devtable", app) as cl:
params = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
response = conduct_api_call(cl, RepositoryAutoPrunePolicy, "GET", params).json
assert response["method"] == "number_of_tags"
assert response["value"] == 10
def test_update_repo_policy(initialized_db, app):
policies = model.autoprune.get_repository_autoprune_policies_by_repo_name("devtable", "simple")
assert len(policies) == 1
policy_uuid = policies[0].uuid
with client_with_identity("devtable", app) as cl:
params_for_update = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
conduct_api_call(
cl,
RepositoryAutoPrunePolicy,
"PUT",
params_for_update,
{"method": "creation_date", "value": "2w"},
expected_code=204,
)
# Make another request asserting it was updated
params = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
get_response = conduct_api_call(cl, RepositoryAutoPrunePolicy, "GET", params).json
assert get_response["method"] == "creation_date"
assert get_response["value"] == "2w"
# Check audit log was created
logs = list(get_latest_logs_query(namespace="devtable"))
log_kinds = get_log_entry_kinds()
log = None
for l in logs:
if l.kind == log_kinds["update_repository_autoprune_policy"]:
log = l
break
assert log is not None
assert json.loads(log.metadata_json)["method"] == "creation_date"
assert json.loads(log.metadata_json)["value"] == "2w"
assert json.loads(log.metadata_json)["namespace"] == "devtable"
def test_update_repo_policy_nonexistent_policy(initialized_db, app):
with client_with_identity("devtable", app) as cl:
params_for_update = {"repository": "devtable/simple", "policy_uuid": "doesnotexist"}
conduct_api_call(
cl,
RepositoryAutoPrunePolicy,
"PUT",
params_for_update,
{"method": "creation_date", "value": "2w"},
expected_code=404,
)
def test_delete_repo_policy(initialized_db, app):
policies = model.autoprune.get_repository_autoprune_policies_by_repo_name("devtable", "simple")
assert len(policies) == 1
policy_uuid = policies[0].uuid
with client_with_identity("devtable", app) as cl:
params_for_delete = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
conduct_api_call(
cl,
RepositoryAutoPrunePolicy,
"DELETE",
params_for_delete,
expected_code=200,
)
params = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
conduct_api_call(
cl,
RepositoryAutoPrunePolicy,
"GET",
params,
expected_code=404,
)
# Check audit log was created
logs = list(get_latest_logs_query(namespace="devtable"))
log_kinds = get_log_entry_kinds()
log = None
for l in logs:
if l.kind == log_kinds["delete_repository_autoprune_policy"]:
log = l
break
assert log is not None
assert json.loads(log.metadata_json)["policy_uuid"] == policy_uuid
assert json.loads(log.metadata_json)["namespace"] == "devtable"
def test_delete_repo_policy_nonexistent_policy(initialized_db, app):
with client_with_identity("devtable", app) as cl:
params_for_delete = {
"repository": "testorgforautoprune/autoprunerepo",
"policy_uuid": "doesnotexist",
}
conduct_api_call(
cl,
RepositoryAutoPrunePolicy,
"DELETE",
params_for_delete,
expected_code=404,
)

View File

@ -6270,6 +6270,182 @@ SECURITY_TESTS: List[
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "devtable", 404), (UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "devtable", 404),
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "freshuser", 404), (UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "freshuser", 404),
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "reader", 404), (UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "reader", 404),
(
RepositoryAutoPrunePolicies,
"GET",
{"repository": "testorgforautoprune/autoprunerepo"},
None,
None,
401,
),
(
RepositoryAutoPrunePolicies,
"GET",
{"repository": "testorgforautoprune/autoprunerepo"},
None,
"devtable",
200,
),
(
RepositoryAutoPrunePolicies,
"GET",
{"repository": "testorgforautoprune/unknown"},
None,
"devtable",
404,
),
(
RepositoryAutoPrunePolicies,
"GET",
{"repository": "testorgforautoprune/autoprunerepo"},
None,
"freshuser",
403,
),
(
RepositoryAutoPrunePolicies,
"GET",
{"repository": "testorgforautoprune/autoprunerepo"},
None,
"reader",
403,
),
(
RepositoryAutoPrunePolicies,
"POST",
{"repository": "testorgforautoprune/autoprunerepo"},
{"method": "number_of_tags", "value": 10},
None,
401,
),
(
RepositoryAutoPrunePolicies,
"POST",
{"repository": "testorgforautoprune/autoprunerepo"},
{"method": "creation_date", "value": "2w"},
"devtable",
201,
),
(
RepositoryAutoPrunePolicies,
"POST",
{"repository": "testorgforautoprune/unknown"},
{"method": "number_of_tags", "value": 10},
"devtable",
404,
),
(
RepositoryAutoPrunePolicies,
"POST",
{"repository": "testorgforautoprune/autoprunerepo"},
{"method": "number_of_tags", "value": 10},
"freshuser",
403,
),
(
RepositoryAutoPrunePolicies,
"POST",
{"repository": "testorgforautoprune/autoprunerepo"},
{"method": "number_of_tags", "value": 10},
"reader",
403,
),
(
RepositoryAutoPrunePolicy,
"GET",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
None,
None,
401,
),
(
RepositoryAutoPrunePolicy,
"GET",
{"repository": "testorgforautoprune/unknown", "policy_uuid": "some_uuid"},
None,
"devtable",
404,
),
(
RepositoryAutoPrunePolicy,
"GET",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
None,
"freshuser",
403,
),
(
RepositoryAutoPrunePolicy,
"GET",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
None,
"reader",
403,
),
(
RepositoryAutoPrunePolicy,
"PUT",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
{"method": "number_of_tags", "value": 10},
None,
401,
),
(
RepositoryAutoPrunePolicy,
"PUT",
{"repository": "testorgforautoprune/unknown", "policy_uuid": "some_uuid"},
{"method": "number_of_tags", "value": 10},
"devtable",
404,
),
(
RepositoryAutoPrunePolicy,
"PUT",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
{"method": "number_of_tags", "value": 10},
"freshuser",
403,
),
(
RepositoryAutoPrunePolicy,
"PUT",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
{"method": "number_of_tags", "value": 10},
"reader",
403,
),
(
RepositoryAutoPrunePolicy,
"DELETE",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
None,
None,
401,
),
(
RepositoryAutoPrunePolicy,
"DELETE",
{"repository": "testorgforautoprune/unknown", "policy_uuid": "some_uuid"},
None,
"devtable",
404,
),
(
RepositoryAutoPrunePolicy,
"DELETE",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
None,
"freshuser",
403,
),
(
RepositoryAutoPrunePolicy,
"DELETE",
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
None,
"reader",
403,
),
] ]

View File

@ -31,14 +31,14 @@ def test_list_all_users(disabled, app):
def test_list_all_orgs(app): def test_list_all_orgs(app):
with client_with_identity("devtable", app) as cl: with client_with_identity("devtable", app) as cl:
result = conduct_api_call(cl, SuperUserOrganizationList, "GET", None, None, 200).json result = conduct_api_call(cl, SuperUserOrganizationList, "GET", None, None, 200).json
assert len(result["organizations"]) == 6 assert len(result["organizations"]) == 7
def test_paginate_orgs(app): def test_paginate_orgs(app):
with client_with_identity("devtable", app) as cl: with client_with_identity("devtable", app) as cl:
params = {"limit": 3} params = {"limit": 4}
firstResult = conduct_api_call(cl, SuperUserOrganizationList, "GET", params, None, 200).json firstResult = conduct_api_call(cl, SuperUserOrganizationList, "GET", params, None, 200).json
assert len(firstResult["organizations"]) == 3 assert len(firstResult["organizations"]) == 4
assert firstResult["next_page"] is not None assert firstResult["next_page"] is not None
params["next_page"] = firstResult["next_page"] params["next_page"] = firstResult["next_page"]
secondResult = conduct_api_call( secondResult = conduct_api_call(
@ -50,13 +50,13 @@ def test_paginate_orgs(app):
def test_paginate_test_list_all_users(app): def test_paginate_test_list_all_users(app):
with client_with_identity("devtable", app) as cl: with client_with_identity("devtable", app) as cl:
params = {"limit": 6} params = {"limit": 7}
firstResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json firstResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json
assert len(firstResult["users"]) == 6 assert len(firstResult["users"]) == 7
assert firstResult["next_page"] is not None assert firstResult["next_page"] is not None
params["next_page"] = firstResult["next_page"] params["next_page"] = firstResult["next_page"]
secondResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json secondResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json
assert len(secondResult["users"]) == 5 assert len(secondResult["users"]) == 4
assert secondResult.get("next_page", None) is None assert secondResult.get("next_page", None) is None

View File

@ -70,7 +70,10 @@ from data.decorators import is_deprecated_model
from data.encryption import FieldEncrypter from data.encryption import FieldEncrypter
from data.fields import Credential from data.fields import Credential
from data.logs_model import logs_model from data.logs_model import logs_model
from data.model.autoprune import create_namespace_autoprune_policy from data.model.autoprune import (
create_namespace_autoprune_policy,
create_repository_autoprune_policy,
)
from data.queue import WorkQueue from data.queue import WorkQueue
from data.registry_model import registry_model from data.registry_model import registry_model
from data.registry_model.datatypes import RepositoryReference from data.registry_model.datatypes import RepositoryReference
@ -464,6 +467,10 @@ def initialize_database():
LogEntryKind.create(name="update_namespace_autoprune_policy") LogEntryKind.create(name="update_namespace_autoprune_policy")
LogEntryKind.create(name="delete_namespace_autoprune_policy") LogEntryKind.create(name="delete_namespace_autoprune_policy")
LogEntryKind.create(name="create_repository_autoprune_policy")
LogEntryKind.create(name="update_repository_autoprune_policy")
LogEntryKind.create(name="delete_repository_autoprune_policy")
ImageStorageLocation.create(name="local_eu") ImageStorageLocation.create(name="local_eu")
ImageStorageLocation.create(name="local_us") ImageStorageLocation.create(name="local_us")
@ -922,6 +929,25 @@ def populate_database(minimal=False):
"buynlarge", {"method": "creation_date", "value": "5d"}, create_task=True "buynlarge", {"method": "creation_date", "value": "5d"}, create_task=True
) )
org_for_autoprune = model.organization.create_organization(
"testorgforautoprune", "autoprune@devtable.com", new_user_1
)
org_repo = __generate_repository(
org_for_autoprune,
"autoprunerepo",
"Repository owned by an org.",
False,
[],
(4, [], ["latest", "prod"]),
)
create_repository_autoprune_policy(
"devtable", simple_repo.name, {"method": "number_of_tags", "value": 10}, create_task=True
)
create_repository_autoprune_policy(
"public", publicrepo.name, {"method": "creation_date", "value": "5d"}, create_task=True
)
liborg = model.organization.create_organization( liborg = model.organization.create_organization(
"library", "quay+library@devtable.com", new_user_1 "library", "quay+library@devtable.com", new_user_1
) )

View File

@ -970,11 +970,11 @@ class TestDeleteNamespace(ApiTestCase):
def test_deletenamespaces(self): def test_deletenamespaces(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
# Try to first delete the user. Since they are the sole admin of three orgs, it should fail. # Try to first delete the user. Since they are the sole admin of five orgs, it should fail.
with check_transitive_modifications(): with check_transitive_modifications():
self.deleteResponse(User, expected_code=400) self.deleteResponse(User, expected_code=400)
# Delete the three orgs, checking in between. # Delete the five orgs, checking in between.
with check_transitive_modifications(): with check_transitive_modifications():
self.deleteEmptyResponse( self.deleteEmptyResponse(
Organization, params=dict(orgname=ORGANIZATION), expected_code=204 Organization, params=dict(orgname=ORGANIZATION), expected_code=204
@ -985,9 +985,14 @@ class TestDeleteNamespace(ApiTestCase):
) )
self.deleteResponse(User, expected_code=400) # Should still fail. self.deleteResponse(User, expected_code=400) # Should still fail.
self.deleteEmptyResponse(Organization, params=dict(orgname="titi"), expected_code=204) self.deleteEmptyResponse(Organization, params=dict(orgname="titi"), expected_code=204)
self.deleteResponse(User, expected_code=400) # Should still fail.
self.deleteEmptyResponse( self.deleteEmptyResponse(
Organization, params=dict(orgname="proxyorg"), expected_code=204 Organization, params=dict(orgname="proxyorg"), expected_code=204
) )
self.deleteResponse(User, expected_code=400) # Should still fail.
self.deleteEmptyResponse(
Organization, params=dict(orgname="testorgforautoprune"), expected_code=204
)
# Add some queue items for the user. # Add some queue items for the user.
notification_queue.put([ADMIN_ACCESS_USER, "somerepo", "somename"], "{}") notification_queue.put([ADMIN_ACCESS_USER, "somerepo", "somename"], "{}")

View File

@ -0,0 +1,157 @@
/// <reference types="cypress" />
describe('Repository settings - Repository autoprune policies', () => {
beforeEach(() => {
cy.exec('npm run quay:seed');
cy.request('GET', `${Cypress.env('REACT_QUAY_APP_API_URL')}/csrf_token`)
.then((response) => response.body.csrf_token)
.then((token) => {
cy.loginByCSRF(token);
});
cy.intercept('GET', '/config', {fixture: 'config.json'}).as('getConfig');
});
const attemptCreateTagNumberRepoPolicy = (cy) => {
cy.get('[data-testid="repository-auto-prune-method"]').select(
'By number of tags',
);
cy.get('input[aria-label="number of tags"]').should('have.value', '20');
cy.get('input[aria-label="number of tags"]').type('{end}{backspace}5');
cy.contains('Save').click();
};
const attemptCreateCreationDateRepoPolicy = (cy) => {
cy.get('[data-testid="repository-auto-prune-method"]').select(
'By age of tags',
);
cy.get('input[aria-label="tag creation date value"]').should(
'have.value',
'7',
);
cy.get('select[aria-label="tag creation date unit"]').contains('days');
cy.get('input[aria-label="tag creation date value"]').type(
'2{leftArrow}{backspace}',
);
cy.get('select[aria-label="tag creation date unit"]').select('weeks');
cy.contains('Save').click();
};
it('creates repo policy based on number of tags', () => {
cy.visit('/repository/projectquay/repo1?tab=settings');
cy.contains('Repository Auto-Prune Policies').click();
cy.get('[data-testid="repository-auto-prune-method"]').contains('None');
// Create policy
attemptCreateTagNumberRepoPolicy(cy);
cy.contains('Successfully created repository auto-prune policy');
cy.get('input[aria-label="number of tags"]').should('have.value', '25');
});
it('creates repo policy based on creation date', () => {
cy.visit('/repository/projectquay/repo1?tab=settings');
cy.contains('Repository Auto-Prune Policies').click();
cy.get('[data-testid="repository-auto-prune-method"]').contains('None');
// Create policy
attemptCreateCreationDateRepoPolicy(cy);
cy.contains('Successfully created repository auto-prune policy');
cy.get('input[aria-label="tag creation date value"]').should(
'have.value',
'2',
);
cy.get('select[aria-label="tag creation date unit"]').contains('weeks');
});
it('updates repo policy', () => {
cy.visit('/repository/projectquay/repo1?tab=settings');
cy.contains('Repository Auto-Prune Policies').click();
cy.get('[data-testid="repository-auto-prune-method"]').contains('None');
// Create initial policy
attemptCreateTagNumberRepoPolicy(cy);
cy.contains('Successfully created repository auto-prune policy');
cy.get('input[aria-label="number of tags"]').should('have.value', '25');
// Update policy
attemptCreateCreationDateRepoPolicy(cy);
cy.contains('Successfully updated repository auto-prune policy');
cy.get('input[aria-label="tag creation date value"]').should(
'have.value',
'2',
);
cy.get('select[aria-label="tag creation date unit"]').contains('weeks');
});
it('deletes repo policy', () => {
cy.visit('/repository/projectquay/repo1?tab=settings');
cy.contains('Repository Auto-Prune Policies').click();
cy.get('[data-testid="repository-auto-prune-method"]').contains('None');
// Create initial policy
attemptCreateTagNumberRepoPolicy(cy);
cy.contains('Successfully created repository auto-prune policy');
cy.get('input[aria-label="number of tags"]').should('have.value', '25');
// Delete policy
cy.get('[data-testid="repository-auto-prune-method"]').select('None');
cy.contains('Save').click();
cy.contains('Successfully deleted repository auto-prune policy');
});
it('displays error when failing to load repo policy', () => {
cy.intercept('GET', '**/autoprunepolicy/**', {statusCode: 500}).as(
'getServerFailure',
);
cy.visit('/repository/projectquay/repo1?tab=settings');
cy.contains('Repository Auto-Prune Policies').click();
cy.contains('Unable to complete request');
cy.contains('AxiosError: Request failed with status code 500');
});
it('displays error when failing to create repo policy', () => {
cy.intercept('POST', '**/autoprunepolicy/**', {statusCode: 500}).as(
'getServerFailure',
);
cy.visit('/repository/projectquay/repo1?tab=settings');
cy.contains('Repository Auto-Prune Policies').click();
attemptCreateTagNumberRepoPolicy(cy);
cy.contains('Could not create repository auto-prune policy');
cy.get('button[aria-label="Danger alert details"]').click();
cy.contains('AxiosError: Request failed with status code 500');
});
it('displays error when failing to update repo policy', () => {
cy.intercept('PUT', '**/autoprunepolicy/**', {statusCode: 500}).as(
'getServerFailure',
);
cy.visit('/repository/projectquay/repo1?tab=settings');
cy.contains('Repository Auto-Prune Policies').click();
cy.get('[data-testid="repository-auto-prune-method"]').contains('None');
attemptCreateTagNumberRepoPolicy(cy);
attemptCreateCreationDateRepoPolicy(cy);
cy.contains('Could not update repository auto-prune policy');
cy.get('button[aria-label="Danger alert details"]').click();
cy.contains('AxiosError: Request failed with status code 500');
});
it('displays error when failing to delete repo policy', () => {
cy.intercept('DELETE', '**/autoprunepolicy/**', {statusCode: 500}).as(
'getServerFailure',
);
cy.visit('/repository/projectquay/repo1?tab=settings');
cy.contains('Repository Auto-Prune Policies').click();
cy.get('[data-testid="repository-auto-prune-method"]').contains('None');
attemptCreateTagNumberRepoPolicy(cy);
cy.contains('Successfully created repository auto-prune policy');
cy.get('input[aria-label="number of tags"]').should('have.value', '25');
cy.get('[data-testid="repository-auto-prune-method"]').select('None');
cy.contains('Save').click();
cy.contains('Could not delete repository auto-prune policy');
cy.get('button[aria-label="Danger alert details"]').click();
cy.contains('AxiosError: Request failed with status code 500');
});
});

View File

@ -60,6 +60,8 @@ ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_r
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_repository_id_repository;
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_pull_robot_id_user; ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_pull_robot_id_user;
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_access_token_id_accesstoken; ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_access_token_id_accesstoken;
ALTER TABLE IF EXISTS ONLY public.repositoryautoprunepolicy DROP CONSTRAINT IF EXISTS fk_repositoryautoprunepolicy_repository_id_repository;
ALTER TABLE IF EXISTS ONLY public.repositoryautoprunepolicy DROP CONSTRAINT IF EXISTS fk_repositoryautoprunepolicy_namespace_id_user;
ALTER TABLE IF EXISTS ONLY public.repositoryauthorizedemail DROP CONSTRAINT IF EXISTS fk_repositoryauthorizedemail_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.repositoryauthorizedemail DROP CONSTRAINT IF EXISTS fk_repositoryauthorizedemail_repository_id_repository;
ALTER TABLE IF EXISTS ONLY public.repositoryactioncount DROP CONSTRAINT IF EXISTS fk_repositoryactioncount_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.repositoryactioncount DROP CONSTRAINT IF EXISTS fk_repositoryactioncount_repository_id_repository;
ALTER TABLE IF EXISTS ONLY public.repository DROP CONSTRAINT IF EXISTS fk_repository_visibility_id_visibility; ALTER TABLE IF EXISTS ONLY public.repository DROP CONSTRAINT IF EXISTS fk_repository_visibility_id_visibility;
@ -259,6 +261,9 @@ DROP INDEX IF EXISTS public.repositorybuild_queue_id;
DROP INDEX IF EXISTS public.repositorybuild_pull_robot_id; DROP INDEX IF EXISTS public.repositorybuild_pull_robot_id;
DROP INDEX IF EXISTS public.repositorybuild_logs_archived; DROP INDEX IF EXISTS public.repositorybuild_logs_archived;
DROP INDEX IF EXISTS public.repositorybuild_access_token_id; DROP INDEX IF EXISTS public.repositorybuild_access_token_id;
DROP INDEX IF EXISTS public.repositoryautoprunepolicy_uuid;
DROP INDEX IF EXISTS public.repositoryautoprunepolicy_repository_id;
DROP INDEX IF EXISTS public.repositoryautoprunepolicy_namespace_id;
DROP INDEX IF EXISTS public.repositoryauthorizedemail_repository_id; DROP INDEX IF EXISTS public.repositoryauthorizedemail_repository_id;
DROP INDEX IF EXISTS public.repositoryauthorizedemail_email_repository_id; DROP INDEX IF EXISTS public.repositoryauthorizedemail_email_repository_id;
DROP INDEX IF EXISTS public.repositoryauthorizedemail_code; DROP INDEX IF EXISTS public.repositoryauthorizedemail_code;
@ -512,6 +517,7 @@ ALTER TABLE IF EXISTS ONLY public.repositorynotification DROP CONSTRAINT IF EXIS
ALTER TABLE IF EXISTS ONLY public.repositorykind DROP CONSTRAINT IF EXISTS pk_repositorykind; ALTER TABLE IF EXISTS ONLY public.repositorykind DROP CONSTRAINT IF EXISTS pk_repositorykind;
ALTER TABLE IF EXISTS ONLY public.repositorybuildtrigger DROP CONSTRAINT IF EXISTS pk_repositorybuildtrigger; ALTER TABLE IF EXISTS ONLY public.repositorybuildtrigger DROP CONSTRAINT IF EXISTS pk_repositorybuildtrigger;
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS pk_repositorybuild; ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS pk_repositorybuild;
ALTER TABLE IF EXISTS ONLY public.repositoryautoprunepolicy DROP CONSTRAINT IF EXISTS pk_repositoryautoprunepolicyid;
ALTER TABLE IF EXISTS ONLY public.repositoryauthorizedemail DROP CONSTRAINT IF EXISTS pk_repositoryauthorizedemail; ALTER TABLE IF EXISTS ONLY public.repositoryauthorizedemail DROP CONSTRAINT IF EXISTS pk_repositoryauthorizedemail;
ALTER TABLE IF EXISTS ONLY public.repositoryactioncount DROP CONSTRAINT IF EXISTS pk_repositoryactioncount; ALTER TABLE IF EXISTS ONLY public.repositoryactioncount DROP CONSTRAINT IF EXISTS pk_repositoryactioncount;
ALTER TABLE IF EXISTS ONLY public.repository DROP CONSTRAINT IF EXISTS pk_repository; ALTER TABLE IF EXISTS ONLY public.repository DROP CONSTRAINT IF EXISTS pk_repository;
@ -617,6 +623,7 @@ ALTER TABLE IF EXISTS public.repositorynotification ALTER COLUMN id DROP DEFAULT
ALTER TABLE IF EXISTS public.repositorykind ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.repositorykind ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.repositorybuildtrigger ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.repositorybuildtrigger ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.repositorybuild ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.repositorybuild ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.repositoryautoprunepolicy ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.repositoryauthorizedemail ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.repositoryauthorizedemail ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.repositoryactioncount ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.repositoryactioncount ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.repository ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.repository ALTER COLUMN id DROP DEFAULT;
@ -755,6 +762,8 @@ DROP SEQUENCE IF EXISTS public.repositorybuildtrigger_id_seq;
DROP TABLE IF EXISTS public.repositorybuildtrigger; DROP TABLE IF EXISTS public.repositorybuildtrigger;
DROP SEQUENCE IF EXISTS public.repositorybuild_id_seq; DROP SEQUENCE IF EXISTS public.repositorybuild_id_seq;
DROP TABLE IF EXISTS public.repositorybuild; DROP TABLE IF EXISTS public.repositorybuild;
DROP SEQUENCE IF EXISTS public.repositoryautoprunepolicy_id_seq;
DROP TABLE IF EXISTS public.repositoryautoprunepolicy;
DROP SEQUENCE IF EXISTS public.repositoryauthorizedemail_id_seq; DROP SEQUENCE IF EXISTS public.repositoryauthorizedemail_id_seq;
DROP TABLE IF EXISTS public.repositoryauthorizedemail; DROP TABLE IF EXISTS public.repositoryauthorizedemail;
DROP SEQUENCE IF EXISTS public.repositoryactioncount_id_seq; DROP SEQUENCE IF EXISTS public.repositoryactioncount_id_seq;
@ -3542,6 +3551,43 @@ ALTER TABLE public.repositoryauthorizedemail_id_seq OWNER TO quay;
ALTER SEQUENCE public.repositoryauthorizedemail_id_seq OWNED BY public.repositoryauthorizedemail.id; ALTER SEQUENCE public.repositoryauthorizedemail_id_seq OWNED BY public.repositoryauthorizedemail.id;
--
-- Name: repositoryautoprunepolicy; Type: TABLE; Schema: public; Owner: quay
--
CREATE TABLE public.repositoryautoprunepolicy (
id integer NOT NULL,
uuid character varying(36) NOT NULL,
repository_id integer NOT NULL,
namespace_id integer NOT NULL,
policy text NOT NULL
);
ALTER TABLE public.repositoryautoprunepolicy OWNER TO quay;
--
-- Name: repositoryautoprunepolicy_id_seq; Type: SEQUENCE; Schema: public; Owner: quay
--
CREATE SEQUENCE public.repositoryautoprunepolicy_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.repositoryautoprunepolicy_id_seq OWNER TO quay;
--
-- Name: repositoryautoprunepolicy_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: quay
--
ALTER SEQUENCE public.repositoryautoprunepolicy_id_seq OWNED BY public.repositoryautoprunepolicy.id;
-- --
-- Name: repositorybuild; Type: TABLE; Schema: public; Owner: quay -- Name: repositorybuild; Type: TABLE; Schema: public; Owner: quay
-- --
@ -5309,6 +5355,13 @@ ALTER TABLE ONLY public.repositoryactioncount ALTER COLUMN id SET DEFAULT nextva
ALTER TABLE ONLY public.repositoryauthorizedemail ALTER COLUMN id SET DEFAULT nextval('public.repositoryauthorizedemail_id_seq'::regclass); ALTER TABLE ONLY public.repositoryauthorizedemail ALTER COLUMN id SET DEFAULT nextval('public.repositoryauthorizedemail_id_seq'::regclass);
--
-- Name: repositoryautoprunepolicy id; Type: DEFAULT; Schema: public; Owner: quay
--
ALTER TABLE ONLY public.repositoryautoprunepolicy ALTER COLUMN id SET DEFAULT nextval('public.repositoryautoprunepolicy_id_seq'::regclass);
-- --
-- Name: repositorybuild id; Type: DEFAULT; Schema: public; Owner: quay -- Name: repositorybuild id; Type: DEFAULT; Schema: public; Owner: quay
-- --
@ -5570,7 +5623,7 @@ COPY public.accesstokenkind (id, name) FROM stdin;
-- --
COPY public.alembic_version (version_num) FROM stdin; COPY public.alembic_version (version_num) FROM stdin;
41d15c93c299 b4da5b09c8df
\. \.
@ -6242,6 +6295,9 @@ COPY public.logentrykind (id, name) FROM stdin;
105 push_repo_failed 105 push_repo_failed
106 pull_repo_failed 106 pull_repo_failed
107 delete_tag_failed 107 delete_tag_failed
108 create_repository_autoprune_policy
109 update_repository_autoprune_policy
110 delete_repository_autoprune_policy
\. \.
@ -6518,8 +6574,8 @@ COPY public.quayservice (id, name) FROM stdin;
-- --
COPY public.queueitem (id, queue_name, body, available_after, available, processing_expires, retries_remaining, state_id) FROM stdin; COPY public.queueitem (id, queue_name, body, available_after, available, processing_expires, retries_remaining, state_id) FROM stdin;
1 namespacegc/2/ {"marker_id": 1, "original_username": "quay"} 2023-11-07 19:57:36.102126 t 2023-11-07 22:52:36.037751 5 e90498e4-d92b-487a-90a3-1ab34388af7c 2 namespacegc/3/ {"marker_id": 2, "original_username": "clair"} 2024-02-20 14:01:29.891875 t 2024-02-20 16:56:29.836048 5 ff6a0b72-d8cc-4efe-ad83-70225ac325e1
2 namespacegc/3/ {"marker_id": 2, "original_username": "clair"} 2023-11-07 19:57:37.766105 t 2023-11-07 22:52:37.740851 5 9ccfcc76-8277-4dc0-8a14-b37440753124 1 namespacegc/2/ {"marker_id": 1, "original_username": "quay"} 2024-02-20 14:01:34.91615 t 2024-02-20 16:56:34.901017 5 6e996e00-e92b-45a9-962f-7e7f3cdb9e63
\. \.
@ -6928,6 +6984,14 @@ COPY public.repositoryauthorizedemail (id, repository_id, email, code, confirmed
\. \.
--
-- Data for Name: repositoryautoprunepolicy; Type: TABLE DATA; Schema: public; Owner: quay
--
COPY public.repositoryautoprunepolicy (id, uuid, repository_id, namespace_id, policy) FROM stdin;
\.
-- --
-- Data for Name: repositorybuild; Type: TABLE DATA; Schema: public; Owner: quay -- Data for Name: repositorybuild; Type: TABLE DATA; Schema: public; Owner: quay
-- --
@ -8084,7 +8148,7 @@ SELECT pg_catalog.setval('public.logentry_id_seq', 1, false);
-- Name: logentrykind_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay -- Name: logentrykind_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
-- --
SELECT pg_catalog.setval('public.logentrykind_id_seq', 107, true); SELECT pg_catalog.setval('public.logentrykind_id_seq', 110, true);
-- --
@ -8168,7 +8232,7 @@ SELECT pg_catalog.setval('public.namespacegeorestriction_id_seq', 1, false);
-- Name: notification_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay -- Name: notification_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
-- --
SELECT pg_catalog.setval('public.notification_id_seq', 1, false); SELECT pg_catalog.setval('public.notification_id_seq', 1, true);
-- --
@ -8325,6 +8389,13 @@ SELECT pg_catalog.setval('public.repositoryactioncount_id_seq', 157, true);
SELECT pg_catalog.setval('public.repositoryauthorizedemail_id_seq', 1, true); SELECT pg_catalog.setval('public.repositoryauthorizedemail_id_seq', 1, true);
--
-- Name: repositoryautoprunepolicy_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
--
SELECT pg_catalog.setval('public.repositoryautoprunepolicy_id_seq', 1, false);
-- --
-- Name: repositorybuild_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay -- Name: repositorybuild_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
-- --
@ -8406,14 +8477,14 @@ SELECT pg_catalog.setval('public.role_id_seq', 3, true);
-- Name: servicekey_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay -- Name: servicekey_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
-- --
SELECT pg_catalog.setval('public.servicekey_id_seq', 1, false); SELECT pg_catalog.setval('public.servicekey_id_seq', 1, true);
-- --
-- Name: servicekeyapproval_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay -- Name: servicekeyapproval_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
-- --
SELECT pg_catalog.setval('public.servicekeyapproval_id_seq', 1, false); SELECT pg_catalog.setval('public.servicekeyapproval_id_seq', 1, true);
-- --
@ -9131,6 +9202,14 @@ ALTER TABLE ONLY public.repositoryauthorizedemail
ADD CONSTRAINT pk_repositoryauthorizedemail PRIMARY KEY (id); ADD CONSTRAINT pk_repositoryauthorizedemail PRIMARY KEY (id);
--
-- Name: repositoryautoprunepolicy pk_repositoryautoprunepolicyid; Type: CONSTRAINT; Schema: public; Owner: quay
--
ALTER TABLE ONLY public.repositoryautoprunepolicy
ADD CONSTRAINT pk_repositoryautoprunepolicyid PRIMARY KEY (id);
-- --
-- Name: repositorybuild pk_repositorybuild; Type: CONSTRAINT; Schema: public; Owner: quay -- Name: repositorybuild pk_repositorybuild; Type: CONSTRAINT; Schema: public; Owner: quay
-- --
@ -10936,6 +11015,27 @@ CREATE UNIQUE INDEX repositoryauthorizedemail_email_repository_id ON public.repo
CREATE INDEX repositoryauthorizedemail_repository_id ON public.repositoryauthorizedemail USING btree (repository_id); CREATE INDEX repositoryauthorizedemail_repository_id ON public.repositoryauthorizedemail USING btree (repository_id);
--
-- Name: repositoryautoprunepolicy_namespace_id; Type: INDEX; Schema: public; Owner: quay
--
CREATE UNIQUE INDEX repositoryautoprunepolicy_namespace_id ON public.repositoryautoprunepolicy USING btree (namespace_id);
--
-- Name: repositoryautoprunepolicy_repository_id; Type: INDEX; Schema: public; Owner: quay
--
CREATE UNIQUE INDEX repositoryautoprunepolicy_repository_id ON public.repositoryautoprunepolicy USING btree (repository_id);
--
-- Name: repositoryautoprunepolicy_uuid; Type: INDEX; Schema: public; Owner: quay
--
CREATE UNIQUE INDEX repositoryautoprunepolicy_uuid ON public.repositoryautoprunepolicy USING btree (uuid);
-- --
-- Name: repositorybuild_access_token_id; Type: INDEX; Schema: public; Owner: quay -- Name: repositorybuild_access_token_id; Type: INDEX; Schema: public; Owner: quay
-- --
@ -12411,6 +12511,22 @@ ALTER TABLE ONLY public.repositoryauthorizedemail
ADD CONSTRAINT fk_repositoryauthorizedemail_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); ADD CONSTRAINT fk_repositoryauthorizedemail_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id);
--
-- Name: repositoryautoprunepolicy fk_repositoryautoprunepolicy_namespace_id_user; Type: FK CONSTRAINT; Schema: public; Owner: quay
--
ALTER TABLE ONLY public.repositoryautoprunepolicy
ADD CONSTRAINT fk_repositoryautoprunepolicy_namespace_id_user FOREIGN KEY (namespace_id) REFERENCES public."user"(id);
--
-- Name: repositoryautoprunepolicy fk_repositoryautoprunepolicy_repository_id_repository; Type: FK CONSTRAINT; Schema: public; Owner: quay
--
ALTER TABLE ONLY public.repositoryautoprunepolicy
ADD CONSTRAINT fk_repositoryautoprunepolicy_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id);
-- --
-- Name: repositorybuild fk_repositorybuild_access_token_id_accesstoken; Type: FK CONSTRAINT; Schema: public; Owner: quay -- Name: repositorybuild fk_repositorybuild_access_token_id_accesstoken; Type: FK CONSTRAINT; Schema: public; Owner: quay
-- --

View File

@ -10,15 +10,20 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
export function useNamespaceAutoPrunePolicies( export function useNamespaceAutoPrunePolicies(
namespace: string, namespace: string,
isUser: boolean, isUser: boolean,
isEnabled: boolean = true,
) { ) {
const { const {
data: policies, data: nsPolicies,
isLoading, isLoading,
error, error,
isSuccess, isSuccess,
dataUpdatedAt, dataUpdatedAt,
} = useQuery(['namespace', 'autoprunepolicies', namespace], ({signal}) => } = useQuery(
fetchNamespaceAutoPrunePolicies(namespace, isUser, signal), ['namespace', 'autoprunepolicies', namespace],
({signal}) => fetchNamespaceAutoPrunePolicies(namespace, isUser, signal),
{
enabled: isEnabled,
},
); );
return { return {
@ -26,7 +31,7 @@ export function useNamespaceAutoPrunePolicies(
isSuccess, isSuccess,
isLoading, isLoading,
dataUpdatedAt, dataUpdatedAt,
policies, nsPolicies,
}; };
} }

View File

@ -0,0 +1,129 @@
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {
RepositoryAutoPrunePolicy,
createRepositoryAutoPrunePolicy,
deleteRepositoryAutoPrunePolicy,
fetchRepositoryAutoPrunePolicies,
updateRepositoryAutoPrunePolicy,
} from 'src/resources/RepositoryAutoPruneResource';
export function useFetchRepositoryAutoPrunePolicies(
organizationName: string,
repoName: string,
) {
const {
data: repoPolicies,
isLoading: isLoadingRepoPolicies,
error: errorFetchingRepoPolicies,
isSuccess: successFetchingRepoPolicies,
dataUpdatedAt: repoPoliciesDataUpdatedAt,
} = useQuery(
['repositoryautoprunepolicies', organizationName, repoName],
({signal}) =>
fetchRepositoryAutoPrunePolicies(organizationName, repoName, signal),
);
return {
errorFetchingRepoPolicies,
successFetchingRepoPolicies,
isLoadingRepoPolicies,
repoPoliciesDataUpdatedAt,
repoPolicies,
};
}
export function useCreateRepositoryAutoPrunePolicy(
organizationName: string,
repoName: string,
) {
const queryClient = useQueryClient();
const {
mutate: createRepoPolicy,
isSuccess: successRepoPolicyCreation,
isError: errorRepoPolicyCreation,
error: errorDetailsRepoPolicyCreation,
} = useMutation(
async (policy: RepositoryAutoPrunePolicy) =>
createRepositoryAutoPrunePolicy(organizationName, repoName, policy),
{
onSuccess: () => {
queryClient.invalidateQueries([
'repositoryautoprunepolicies',
organizationName,
repoName,
]);
},
},
);
return {
createRepoPolicy,
successRepoPolicyCreation,
errorRepoPolicyCreation,
errorDetailsRepoPolicyCreation,
};
}
export function useUpdateRepositoryAutoPrunePolicy(
organizationName: string,
repoName: string,
) {
const queryClient = useQueryClient();
const {
mutate: updateRepoPolicy,
isSuccess: successRepoPolicyUpdation,
isError: errorRepoPolicyUpdation,
error: errorDetailsRepoPolicyUpdation,
} = useMutation(
async (policy: RepositoryAutoPrunePolicy) =>
updateRepositoryAutoPrunePolicy(organizationName, repoName, policy),
{
onSuccess: () => {
queryClient.invalidateQueries([
'repositoryautoprunepolicies',
organizationName,
repoName,
]);
},
},
);
return {
updateRepoPolicy,
successRepoPolicyUpdation,
errorRepoPolicyUpdation,
errorDetailsRepoPolicyUpdation,
};
}
export function useDeleteRepositoryAutoPrunePolicy(
organizationName: string,
repoName: string,
) {
const queryClient = useQueryClient();
const {
mutate: deleteRepoPolicy,
isSuccess: successRepoPolicyDeletion,
isError: errorRepoPolicyDeletion,
error: errorDetailsRepoPolicyDeletion,
} = useMutation(
async (uuid: string) =>
deleteRepositoryAutoPrunePolicy(organizationName, repoName, uuid),
{
onSuccess: () => {
queryClient.invalidateQueries([
'repositoryautoprunepolicies',
organizationName,
repoName,
]);
},
},
);
return {
deleteRepoPolicy,
successRepoPolicyDeletion,
errorRepoPolicyDeletion,
errorDetailsRepoPolicyDeletion,
};
}

View File

@ -0,0 +1,54 @@
import {AxiosResponse} from 'axios';
import axios from 'src/libs/axios';
import {assertHttpCode} from './ErrorHandling';
import {AutoPruneMethod} from './NamespaceAutoPruneResource';
export interface RepositoryAutoPrunePolicy {
method: AutoPruneMethod;
uuid?: string;
value?: string | number;
}
export async function fetchRepositoryAutoPrunePolicies(
organizationName: string,
repoName: string,
signal: AbortSignal,
) {
const repositoryAutoPruneUrl = `/api/v1/repository/${organizationName}/${repoName}/autoprunepolicy/`;
const response: AxiosResponse = await axios.get(repositoryAutoPruneUrl, {
signal,
});
assertHttpCode(response.status, 200);
const res = response.data.policies as RepositoryAutoPrunePolicy[];
return res;
}
export async function createRepositoryAutoPrunePolicy(
organizationName: string,
repoName: string,
policy: RepositoryAutoPrunePolicy,
) {
const repositoryAutoPruneUrl = `/api/v1/repository/${organizationName}/${repoName}/autoprunepolicy/`;
const response = await axios.post(repositoryAutoPruneUrl, policy);
assertHttpCode(response.status, 201);
}
export async function updateRepositoryAutoPrunePolicy(
organizationName: string,
repoName: string,
policy: RepositoryAutoPrunePolicy,
) {
const repositoryAutoPruneUrl = `/api/v1/repository/${organizationName}/${repoName}/autoprunepolicy/${policy.uuid}`;
const response = await axios.put(repositoryAutoPruneUrl, policy);
assertHttpCode(response.status, 204);
}
export async function deleteRepositoryAutoPrunePolicy(
organizationName: string,
repoName: string,
uuid: string,
) {
const repositoryAutoPruneUrl = `/api/v1/repository/${organizationName}/${repoName}/autoprunepolicy/${uuid}`;
const response = await axios.delete(repositoryAutoPruneUrl);
assertHttpCode(response.status, 200);
}

View File

@ -23,7 +23,6 @@ import Settings from './Tabs/Settings/Settings';
import TeamsAndMembershipList from './Tabs/TeamsAndMembership/TeamsAndMembershipList'; import TeamsAndMembershipList from './Tabs/TeamsAndMembership/TeamsAndMembershipList';
import AddNewTeamMemberDrawer from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/AddNewTeamMemberDrawer'; import AddNewTeamMemberDrawer from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/AddNewTeamMemberDrawer';
import ManageMembersList from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList'; import ManageMembersList from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList';
import UsageLogs from 'src/routes/UsageLogs/UsageLogs';
export enum OrganizationDrawerContentType { export enum OrganizationDrawerContentType {
None, None,

View File

@ -50,7 +50,7 @@ export default function AutoPruning(props: AutoPruning) {
error, error,
isSuccess: successFetchingPolicies, isSuccess: successFetchingPolicies,
isLoading, isLoading,
policies, nsPolicies,
dataUpdatedAt, dataUpdatedAt,
} = useNamespaceAutoPrunePolicies(props.org, props.isUser); } = useNamespaceAutoPrunePolicies(props.org, props.isUser);
const { const {
@ -76,8 +76,8 @@ export default function AutoPruning(props: AutoPruning) {
if (successFetchingPolicies) { if (successFetchingPolicies) {
// Currently we only support one policy per namespace but // Currently we only support one policy per namespace but
// this will change in the future. // this will change in the future.
if (policies.length > 0) { if (nsPolicies.length > 0) {
const policy: NamespaceAutoPrunePolicy = policies[0]; const policy: NamespaceAutoPrunePolicy = nsPolicies[0];
setMethod(policy.method); setMethod(policy.method);
setUuid(policy.uuid); setUuid(policy.uuid);
switch (policy.method) { switch (policy.method) {

View File

@ -0,0 +1,426 @@
import {
ActionGroup,
Button,
Flex,
Form,
FormGroup,
FormSelect,
FormSelectOption,
NumberInput,
Spinner,
Title,
FormHelperText,
HelperText,
HelperTextItem,
DataList,
DataListItem,
DataListItemRow,
DataListItemCells,
DataListCell,
Gallery,
} from '@patternfly/react-core';
import {useEffect, useState} from 'react';
import {AlertVariant} from 'src/atoms/AlertState';
import Conditional from 'src/components/empty/Conditional';
import RequestError from 'src/components/errors/RequestError';
import {useAlerts} from 'src/hooks/UseAlerts';
import {useNamespaceAutoPrunePolicies} from 'src/hooks/UseNamespaceAutoPrunePolicies';
import {useOrganization} from 'src/hooks/UseOrganization';
import {
useCreateRepositoryAutoPrunePolicy,
useDeleteRepositoryAutoPrunePolicy,
useFetchRepositoryAutoPrunePolicies,
useUpdateRepositoryAutoPrunePolicy,
} from 'src/hooks/UseRepositoryAutoPrunePolicies';
import {isNullOrUndefined} from 'src/libs/utils';
import {AutoPruneMethod} from 'src/resources/NamespaceAutoPruneResource';
import {RepositoryAutoPrunePolicy} from 'src/resources/RepositoryAutoPruneResource';
import {shorthandTimeUnits} from 'src/routes/OrganizationsList/Organization/Tabs/Settings/AutoPruning';
export default function RepositoryAutoPruning(props: RepositoryAutoPruning) {
const [uuid, setUuid] = useState<string>(null);
const [method, setMethod] = useState<AutoPruneMethod>(AutoPruneMethod.NONE);
const [tagCount, setTagCount] = useState<number>(20);
const [tagCreationDateUnit, setTagCreationDateUnit] = useState<string>('d');
const [tagCreationDateValue, setTagCreationDateValue] = useState<number>(7);
const {addAlert} = useAlerts();
const {organization} = useOrganization(props.organizationName);
const {
error,
isSuccess: successFetchingPolicies,
isLoading,
nsPolicies,
dataUpdatedAt,
} = useNamespaceAutoPrunePolicies(
props.organizationName,
props.isUser,
organization?.is_org_admin || false,
);
const {
errorFetchingRepoPolicies,
successFetchingRepoPolicies,
isLoadingRepoPolicies,
repoPolicies,
repoPoliciesDataUpdatedAt,
} = useFetchRepositoryAutoPrunePolicies(
props.organizationName,
props.repoName,
);
const {
createRepoPolicy,
successRepoPolicyCreation,
errorRepoPolicyCreation,
errorDetailsRepoPolicyCreation,
} = useCreateRepositoryAutoPrunePolicy(
props.organizationName,
props.repoName,
);
const {
updateRepoPolicy,
successRepoPolicyUpdation,
errorRepoPolicyUpdation,
errorDetailsRepoPolicyUpdation,
} = useUpdateRepositoryAutoPrunePolicy(
props.organizationName,
props.repoName,
);
const {
deleteRepoPolicy,
successRepoPolicyDeletion,
errorRepoPolicyDeletion,
errorDetailsRepoPolicyDeletion,
} = useDeleteRepositoryAutoPrunePolicy(
props.organizationName,
props.repoName,
);
useEffect(() => {
if (successFetchingRepoPolicies) {
// Currently we only support one policy per repository but
// this will change in the future.
if (repoPolicies.length > 0) {
const policy: RepositoryAutoPrunePolicy = repoPolicies[0];
setMethod(policy.method);
setUuid(policy.uuid);
switch (policy.method) {
case AutoPruneMethod.TAG_NUMBER: {
setTagCount(policy.value as number);
break;
}
case AutoPruneMethod.TAG_CREATION_DATE: {
const tagAgeValue = (policy.value as string).match(/\d+/g);
const tagAgeUnit = (policy.value as string).match(/[a-zA-Z]+/g);
if (tagAgeValue.length > 0 && tagAgeUnit.length > 0) {
setTagCreationDateValue(Number(tagAgeValue[0]));
setTagCreationDateUnit(tagAgeUnit[0]);
} else {
console.error('Invalid tag age value');
}
break;
}
}
} else {
// If no policy was returned it's possible this was
// after the deletion of the policy, in which all the state
// has to be reset
setUuid(null);
setMethod(AutoPruneMethod.NONE);
setTagCount(20);
setTagCreationDateUnit('d');
setTagCreationDateValue(7);
}
}
}, [
successFetchingRepoPolicies,
successFetchingPolicies,
repoPoliciesDataUpdatedAt,
]);
useEffect(() => {
if (successRepoPolicyCreation) {
addAlert({
title: 'Successfully created repository auto-prune policy',
variant: AlertVariant.Success,
});
}
}, [successRepoPolicyCreation]);
useEffect(() => {
if (successRepoPolicyUpdation) {
addAlert({
title: 'Successfully updated repository auto-prune policy',
variant: AlertVariant.Success,
});
}
}, [successRepoPolicyUpdation]);
useEffect(() => {
if (successRepoPolicyDeletion) {
addAlert({
title: 'Successfully deleted repository auto-prune policy',
variant: AlertVariant.Success,
});
}
}, [successRepoPolicyDeletion]);
useEffect(() => {
if (errorRepoPolicyCreation) {
addAlert({
title: 'Could not create repository auto-prune policy',
variant: AlertVariant.Failure,
message: errorDetailsRepoPolicyCreation.toString(),
});
}
}, [errorRepoPolicyCreation]);
useEffect(() => {
if (errorRepoPolicyUpdation) {
addAlert({
title: 'Could not update repository auto-prune policy',
variant: AlertVariant.Failure,
message: errorDetailsRepoPolicyUpdation.toString(),
});
}
}, [errorRepoPolicyUpdation]);
useEffect(() => {
if (errorRepoPolicyDeletion) {
addAlert({
title: 'Could not delete repository auto-prune policy',
variant: AlertVariant.Failure,
message: errorDetailsRepoPolicyDeletion.toString(),
});
}
}, [errorRepoPolicyDeletion]);
const onSave = (e) => {
e.preventDefault();
let value = null;
switch (method) {
case AutoPruneMethod.TAG_NUMBER:
value = tagCount;
break;
case AutoPruneMethod.TAG_CREATION_DATE:
value = `${String(tagCreationDateValue)}${tagCreationDateUnit}`;
break;
case AutoPruneMethod.NONE:
// Delete the policy is done by setting the method to none
if (!isNullOrUndefined(uuid)) {
deleteRepoPolicy(uuid);
}
return;
default:
// Reaching here indicates programming error, component should always be aware of valid methods
return;
}
if (isNullOrUndefined(uuid)) {
createRepoPolicy({method: method, value: value});
} else {
updateRepoPolicy({uuid: uuid, method: method, value: value});
}
};
if (isLoadingRepoPolicies) {
return <Spinner />;
}
if (!isNullOrUndefined(errorFetchingRepoPolicies)) {
return <RequestError message={errorFetchingRepoPolicies.toString()} />;
}
return (
<>
<Conditional if={nsPolicies !== null && nsPolicies !== undefined}>
<Title headingLevel="h2" style={{paddingBottom: '.5em'}}>
Namespace Auto-Pruning Policies
</Title>
<Gallery>
<DataList
className="pf-v5-u-mb-lg"
aria-label="Simple data list example"
isCompact
>
<DataListItem aria-labelledby="simple-item1">
<DataListItemRow>
<DataListItemCells
dataListCells={
nsPolicies
? [
<DataListCell key="policy-method">
<span id="simple-item1">
{nsPolicies[0]?.method}:
</span>
</DataListCell>,
<DataListCell key="policy-value">
<span id="simple-item1">
{nsPolicies[0]?.value}
</span>
</DataListCell>,
]
: []
}
/>
</DataListItemRow>
</DataListItem>
</DataList>
</Gallery>
</Conditional>
<Title headingLevel="h2" style={{paddingBottom: '.5em'}}>
Repository Auto-Pruning Policies
</Title>
<p style={{paddingBottom: '1em'}}>
Auto-pruning policies automatically delete tags under this repository by
a given method.
</p>
<Form id="autopruning-form" maxWidth="40%">
<FormGroup
isInline
label="Prune Policy - select a method to prune tags"
fieldId="method"
isRequired
>
<FormSelect
placeholder=""
aria-label="repository-auto-prune-method"
data-testid="repository-auto-prune-method"
value={method}
onChange={(_, val) => setMethod(val as AutoPruneMethod)}
>
<FormSelectOption
key={1}
value={AutoPruneMethod.NONE}
label="None"
/>
<FormSelectOption
key={2}
value={AutoPruneMethod.TAG_NUMBER}
label="By number of tags"
/>
<FormSelectOption
key={3}
value={AutoPruneMethod.TAG_CREATION_DATE}
label="By age of tags"
/>
</FormSelect>
<FormHelperText>
<HelperText>
<HelperTextItem>The method used to prune tags.</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
<Conditional if={method === AutoPruneMethod.TAG_NUMBER}>
<FormGroup label="The number of tags to keep." fieldId="" isRequired>
<NumberInput
value={tagCount}
onMinus={() => {
tagCount > 1 ? setTagCount(tagCount - 1) : setTagCount(1);
}}
onChange={(e) => {
const input = (e.target as HTMLInputElement).value;
const value = Number(input);
if (value > 0 && /^\d+$/.test(input)) {
setTagCount(value);
}
}}
onPlus={() => {
setTagCount(tagCount + 1);
}}
inputAriaLabel="number of tags"
minusBtnAriaLabel="minus"
plusBtnAriaLabel="plus"
data-testid="repository-auto-prune-tag-count"
/>
<FormHelperText>
<HelperText>
<HelperTextItem>
All tags sorted by earliest creation date will be deleted
until the repository total falls below the threshold
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
</Conditional>
<Conditional if={method === AutoPruneMethod.TAG_CREATION_DATE}>
<FormGroup
label="Delete tags older than given timespan."
fieldId=""
isRequired
isInline
>
<div style={{display: 'flex'}}>
<NumberInput
value={tagCreationDateValue}
onMinus={() => {
tagCreationDateValue > 1
? setTagCreationDateValue(tagCreationDateValue - 1)
: setTagCreationDateValue(1);
}}
onChange={(e) => {
const input = (e.target as HTMLInputElement).value;
const value = Number(input);
if (value > 0 && /^\d+$/.test(input)) {
setTagCreationDateValue(value);
}
}}
onPlus={() => {
setTagCreationDateValue(tagCreationDateValue + 1);
}}
inputAriaLabel="tag creation date value"
minusBtnAriaLabel="minus"
plusBtnAriaLabel="plus"
data-testid="repository-auto-prune-tag-creation-date-value"
style={{paddingRight: '1em'}}
/>
<FormSelect
placeholder=""
aria-label="tag creation date unit"
data-testid="tag-auto-prune-creation-date-timeunit"
value={tagCreationDateUnit}
onChange={(_, val) => setTagCreationDateUnit(val)}
style={{width: '10em'}}
>
{Object.keys(shorthandTimeUnits).map((key) => (
<FormSelectOption
key={key}
value={key}
label={shorthandTimeUnits[key]}
/>
))}
</FormSelect>
</div>
<FormHelperText>
<HelperText>
<HelperTextItem>
All tags with a creation date earlier than the selected time
period will be deleted
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
</Conditional>
<ActionGroup>
<Flex
justifyContent={{default: 'justifyContentFlexEnd'}}
width="100%"
>
<Button variant="primary" type="submit" onClick={onSave}>
Save
</Button>
</Flex>
</ActionGroup>
</Form>
</>
);
}
interface RepositoryAutoPruning {
organizationName: string;
repoName: string;
isUser: boolean;
}

View File

@ -8,10 +8,14 @@ import Visibility from './Visibility';
import {RepositoryStateForm} from './RepositoryState'; import {RepositoryStateForm} from './RepositoryState';
import {RepositoryDetails} from 'src/resources/RepositoryResource'; import {RepositoryDetails} from 'src/resources/RepositoryResource';
import {useQuayConfig} from 'src/hooks/UseQuayConfig'; import {useQuayConfig} from 'src/hooks/UseQuayConfig';
import RepositoryAutoPruning from 'src/routes/RepositoryDetails/Settings/RepositoryAutoPruning';
import {useOrganization} from 'src/hooks/UseOrganization';
export default function Settings(props: SettingsProps) { export default function Settings(props: SettingsProps) {
const [activeTabIndex, setActiveTabIndex] = useState(0); const [activeTabIndex, setActiveTabIndex] = useState(0);
const config = useQuayConfig(); const config = useQuayConfig();
const {isUserOrganization} = useOrganization(props.org);
const tabs = [ const tabs = [
{ {
name: 'User and robot permissions', name: 'User and robot permissions',
@ -24,6 +28,21 @@ export default function Settings(props: SettingsProps) {
/> />
), ),
}, },
...(config?.features?.AUTO_PRUNE
? [
{
name: 'Repository Auto-Prune Policies',
id: 'repositoryautoprunepolicies',
content: (
<RepositoryAutoPruning
organizationName={props.org}
repoName={props.repo}
isUser={isUserOrganization}
/>
),
},
]
: []),
{ {
name: 'Events and notifications', name: 'Events and notifications',
id: 'eventsandnotifications', id: 'eventsandnotifications',

View File

@ -1,4 +1,6 @@
import logging.config import logging.config
import os
import sys
import time import time
import features import features
@ -35,21 +37,42 @@ class AutoPruneWorker(Worker):
autoprune_task.id, autoprune_task.id,
autoprune_task.namespace, autoprune_task.namespace,
) )
repo_policies = []
try: try:
policies = get_namespace_autoprune_policies_by_id(autoprune_task.namespace) ns_policies = get_namespace_autoprune_policies_by_id(autoprune_task.namespace)
if not policies: if not ns_policies:
# When implementing repo policies, fetch repo policies before deleting the task repo_policies = get_repository_autoprune_policies_by_namespace_id(
delete_autoprune_task(autoprune_task) autoprune_task.namespace
continue )
if not repo_policies:
logger.info(
"deleting autoprune task %s for namespace %s",
autoprune_task.id,
autoprune_task.namespace,
)
delete_autoprune_task(autoprune_task)
continue
execute_namespace_polices( execute_namespace_polices(
policies, ns_policies,
autoprune_task.namespace, autoprune_task.namespace,
FETCH_REPOSITORIES_PAGE_LIMIT, FETCH_REPOSITORIES_PAGE_LIMIT,
FETCH_TAGS_PAGE_LIMIT, FETCH_TAGS_PAGE_LIMIT,
) )
# case: only repo policies exists & no namespace policy
for policy in repo_policies:
repo_id = policy.repository_id
repo = get_repository_by_policy_repo_id(repo_id)
logger.info(
"processing autoprune task %s for repository %s",
autoprune_task.id,
repo.name,
)
execute_policy_on_repo(
policy, repo_id, autoprune_task.namespace, tag_page_limit=100
)
update_autoprune_task(autoprune_task, task_status="success") update_autoprune_task(autoprune_task, task_status="success")
except Exception as err: except Exception as err:
update_autoprune_task(autoprune_task, task_status=f"failure: {str(err)}") update_autoprune_task(autoprune_task, task_status=f"failure: {str(err)}")