mirror of
https://github.com/quay/quay.git
synced 2025-07-30 07:43:13 +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:
committed by
GitHub
parent
29258ae0c7
commit
98811f5397
@ -748,6 +748,7 @@ class User(BaseModel):
|
||||
OrganizationRhSkus,
|
||||
NamespaceAutoPrunePolicy,
|
||||
AutoPruneTaskStatus,
|
||||
RepositoryAutoPrunePolicy,
|
||||
}
|
||||
| appr_classes
|
||||
| v22_classes
|
||||
@ -968,6 +969,7 @@ class Repository(BaseModel):
|
||||
UploadedBlob,
|
||||
QuotaNamespaceSize,
|
||||
QuotaRepositorySize,
|
||||
RepositoryAutoPrunePolicy,
|
||||
}
|
||||
| appr_classes
|
||||
| v22_classes
|
||||
@ -2021,6 +2023,13 @@ class AutoPruneTaskStatus(BaseModel):
|
||||
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
|
||||
# to meet length restrictions.
|
||||
LEGACY_INDEX_MAP = {
|
||||
|
@ -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")
|
||||
)
|
||||
)
|
@ -173,6 +173,26 @@ class InvalidNamespaceException(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
class RepositoryAutoPrunePolicyAlreadyExists(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
class RepositoryAutoPrunePolicyDoesNotExist(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRepositoryAutoPrunePolicy(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRepositoryAutoPruneMethod(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRepositoryException(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
class TooManyLoginAttemptsException(Exception):
|
||||
def __init__(self, message, retry_after):
|
||||
super(TooManyLoginAttemptsException, self).__init__(message)
|
||||
|
@ -2,21 +2,21 @@ import json
|
||||
import logging.config
|
||||
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 (
|
||||
Repository,
|
||||
RepositoryState,
|
||||
User,
|
||||
db_for_update,
|
||||
get_epoch_timestamp_ms,
|
||||
)
|
||||
from data.database import Repository
|
||||
from data.database import RepositoryAutoPrunePolicy as RepositoryAutoPrunePolicyTable
|
||||
from data.database import RepositoryState, User, db_for_update, get_epoch_timestamp_ms
|
||||
from data.model import (
|
||||
InvalidNamespaceAutoPruneMethod,
|
||||
InvalidNamespaceAutoPrunePolicy,
|
||||
InvalidNamespaceException,
|
||||
InvalidRepositoryAutoPrunePolicy,
|
||||
InvalidRepositoryException,
|
||||
NamespaceAutoPrunePolicyAlreadyExists,
|
||||
NamespaceAutoPrunePolicyDoesNotExist,
|
||||
RepositoryAutoPrunePolicyAlreadyExists,
|
||||
RepositoryAutoPrunePolicyDoesNotExist,
|
||||
db_transaction,
|
||||
log,
|
||||
modelutil,
|
||||
@ -53,6 +53,22 @@ class NamespaceAutoPrunePolicy:
|
||||
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):
|
||||
"""
|
||||
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")
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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]
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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]
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Get the specific autopruning policy for the specified namespace by uuid.
|
||||
@ -128,6 +195,22 @@ def get_namespace_autoprune_policy(orgname, uuid):
|
||||
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):
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Deletes the policy specified by the uuid
|
||||
Deletes the namespace policy specified by the uuid
|
||||
"""
|
||||
|
||||
with db_transaction():
|
||||
@ -217,6 +363,43 @@ def delete_namespace_autoprune_policy(orgname, uuid):
|
||||
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):
|
||||
return (
|
||||
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):
|
||||
return (
|
||||
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)
|
||||
.where(
|
||||
AutoPruneTaskStatus.namespace.not_in(
|
||||
# this basically skips ns if user is not enabled
|
||||
User.select(User.id).where(
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
execute_policy_on_repo(policy, repo, namespace_id, tag_page_limit)
|
||||
for ns_policy in ns_policies:
|
||||
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):
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
if not policies:
|
||||
if not ns_policies:
|
||||
return
|
||||
page_token = None
|
||||
|
||||
@ -453,7 +667,7 @@ def execute_namespace_polices(policies, namespace_id, repository_page_limit=50,
|
||||
)
|
||||
|
||||
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:
|
||||
break
|
||||
|
@ -21,6 +21,7 @@ from data.database import (
|
||||
Repository,
|
||||
RepositoryActionCount,
|
||||
RepositoryAuthorizedEmail,
|
||||
RepositoryAutoPrunePolicy,
|
||||
RepositoryBuild,
|
||||
RepositoryBuildTrigger,
|
||||
RepositoryNotification,
|
||||
@ -105,6 +106,8 @@ def purge_repository(repo, force=False):
|
||||
ManifestSecurityStatus.select().where(ManifestSecurityStatus.repository == repo).count()
|
||||
== 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
|
||||
# the repository.
|
||||
|
@ -52,9 +52,9 @@ def test_load_security_information(indexed_v4, expected_status, initialized_db):
|
||||
@pytest.mark.parametrize(
|
||||
"next_token, expected_next_token, expected_error",
|
||||
[
|
||||
(None, V4ScanToken(56), None),
|
||||
(V4ScanToken(None), V4ScanToken(56), AssertionError),
|
||||
(V4ScanToken(1), V4ScanToken(56), None),
|
||||
(None, V4ScanToken(58), None),
|
||||
(V4ScanToken(None), V4ScanToken(58), AssertionError),
|
||||
(V4ScanToken(1), V4ScanToken(58), None),
|
||||
],
|
||||
)
|
||||
def test_perform_indexing(next_token, expected_next_token, expected_error, initialized_db):
|
||||
|
169
data/test/test_repository_autoprune.py
Normal file
169
data/test/test_repository_autoprune.py
Normal 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
|
@ -5,15 +5,21 @@ from flask import request
|
||||
import features
|
||||
from auth import scopes
|
||||
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.registry_model import registry_model
|
||||
from endpoints.api import (
|
||||
ApiResource,
|
||||
RepositoryParamResource,
|
||||
allow_if_superuser,
|
||||
log_action,
|
||||
nickname,
|
||||
path_param,
|
||||
request_error,
|
||||
require_repo_admin,
|
||||
require_scope,
|
||||
require_user_admin,
|
||||
resource,
|
||||
@ -231,6 +237,233 @@ class OrgAutoPrunePolicy(ApiResource):
|
||||
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/")
|
||||
@show_if(features.AUTO_PRUNE)
|
||||
class UserAutoPrunePolicies(ApiResource):
|
||||
|
@ -7,6 +7,8 @@ from data.model.log import get_latest_logs_query, get_log_entry_kinds
|
||||
from endpoints.api.policy import (
|
||||
OrgAutoPrunePolicies,
|
||||
OrgAutoPrunePolicy,
|
||||
RepositoryAutoPrunePolicies,
|
||||
RepositoryAutoPrunePolicy,
|
||||
UserAutoPrunePolicies,
|
||||
UserAutoPrunePolicy,
|
||||
)
|
||||
@ -345,3 +347,187 @@ def test_delete_user_policy_nonexistent_policy(initialized_db, app):
|
||||
{"orgname": "devtable", "policy_uuid": "doesnotexist"},
|
||||
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,
|
||||
)
|
||||
|
@ -6270,6 +6270,182 @@ SECURITY_TESTS: List[
|
||||
(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, "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,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -31,14 +31,14 @@ def test_list_all_users(disabled, app):
|
||||
def test_list_all_orgs(app):
|
||||
with client_with_identity("devtable", app) as cl:
|
||||
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):
|
||||
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
|
||||
assert len(firstResult["organizations"]) == 3
|
||||
assert len(firstResult["organizations"]) == 4
|
||||
assert firstResult["next_page"] is not None
|
||||
params["next_page"] = firstResult["next_page"]
|
||||
secondResult = conduct_api_call(
|
||||
@ -50,13 +50,13 @@ def test_paginate_orgs(app):
|
||||
|
||||
def test_paginate_test_list_all_users(app):
|
||||
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
|
||||
assert len(firstResult["users"]) == 6
|
||||
assert len(firstResult["users"]) == 7
|
||||
assert firstResult["next_page"] is not None
|
||||
params["next_page"] = firstResult["next_page"]
|
||||
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
|
||||
|
||||
|
||||
|
28
initdb.py
28
initdb.py
@ -70,7 +70,10 @@ from data.decorators import is_deprecated_model
|
||||
from data.encryption import FieldEncrypter
|
||||
from data.fields import Credential
|
||||
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.registry_model import registry_model
|
||||
from data.registry_model.datatypes import RepositoryReference
|
||||
@ -464,6 +467,10 @@ def initialize_database():
|
||||
LogEntryKind.create(name="update_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_us")
|
||||
|
||||
@ -922,6 +929,25 @@ def populate_database(minimal=False):
|
||||
"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(
|
||||
"library", "quay+library@devtable.com", new_user_1
|
||||
)
|
||||
|
@ -970,11 +970,11 @@ class TestDeleteNamespace(ApiTestCase):
|
||||
def test_deletenamespaces(self):
|
||||
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():
|
||||
self.deleteResponse(User, expected_code=400)
|
||||
|
||||
# Delete the three orgs, checking in between.
|
||||
# Delete the five orgs, checking in between.
|
||||
with check_transitive_modifications():
|
||||
self.deleteEmptyResponse(
|
||||
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.deleteEmptyResponse(Organization, params=dict(orgname="titi"), expected_code=204)
|
||||
self.deleteResponse(User, expected_code=400) # Should still fail.
|
||||
self.deleteEmptyResponse(
|
||||
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.
|
||||
notification_queue.put([ADMIN_ACCESS_USER, "somerepo", "somename"], "{}")
|
||||
|
157
web/cypress/e2e/repository-autopruning.cy.ts
Normal file
157
web/cypress/e2e/repository-autopruning.cy.ts
Normal 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');
|
||||
});
|
||||
});
|
@ -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_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.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.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;
|
||||
@ -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_logs_archived;
|
||||
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_email_repository_id;
|
||||
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.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.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.repositoryactioncount DROP CONSTRAINT IF EXISTS pk_repositoryactioncount;
|
||||
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.repositorybuildtrigger 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.repositoryactioncount 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 SEQUENCE IF EXISTS public.repositorybuild_id_seq;
|
||||
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 TABLE IF EXISTS public.repositoryauthorizedemail;
|
||||
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;
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -5570,7 +5623,7 @@ COPY public.accesstokenkind (id, name) 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
|
||||
106 pull_repo_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;
|
||||
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"} 2023-11-07 19:57:37.766105 t 2023-11-07 22:52:37.740851 5 9ccfcc76-8277-4dc0-8a14-b37440753124
|
||||
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
|
||||
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
|
||||
--
|
||||
@ -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
|
||||
--
|
||||
|
||||
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
|
||||
--
|
||||
|
||||
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);
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -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
|
||||
--
|
||||
|
||||
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
|
||||
--
|
||||
|
||||
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);
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -12411,6 +12511,22 @@ ALTER TABLE ONLY public.repositoryauthorizedemail
|
||||
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
|
||||
--
|
||||
|
@ -10,15 +10,20 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
export function useNamespaceAutoPrunePolicies(
|
||||
namespace: string,
|
||||
isUser: boolean,
|
||||
isEnabled: boolean = true,
|
||||
) {
|
||||
const {
|
||||
data: policies,
|
||||
data: nsPolicies,
|
||||
isLoading,
|
||||
error,
|
||||
isSuccess,
|
||||
dataUpdatedAt,
|
||||
} = useQuery(['namespace', 'autoprunepolicies', namespace], ({signal}) =>
|
||||
fetchNamespaceAutoPrunePolicies(namespace, isUser, signal),
|
||||
} = useQuery(
|
||||
['namespace', 'autoprunepolicies', namespace],
|
||||
({signal}) => fetchNamespaceAutoPrunePolicies(namespace, isUser, signal),
|
||||
{
|
||||
enabled: isEnabled,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
@ -26,7 +31,7 @@ export function useNamespaceAutoPrunePolicies(
|
||||
isSuccess,
|
||||
isLoading,
|
||||
dataUpdatedAt,
|
||||
policies,
|
||||
nsPolicies,
|
||||
};
|
||||
}
|
||||
|
||||
|
129
web/src/hooks/UseRepositoryAutoPrunePolicies.ts
Normal file
129
web/src/hooks/UseRepositoryAutoPrunePolicies.ts
Normal 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,
|
||||
};
|
||||
}
|
54
web/src/resources/RepositoryAutoPruneResource.ts
Normal file
54
web/src/resources/RepositoryAutoPruneResource.ts
Normal 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);
|
||||
}
|
@ -23,7 +23,6 @@ import Settings from './Tabs/Settings/Settings';
|
||||
import TeamsAndMembershipList from './Tabs/TeamsAndMembership/TeamsAndMembershipList';
|
||||
import AddNewTeamMemberDrawer from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/AddNewTeamMemberDrawer';
|
||||
import ManageMembersList from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList';
|
||||
import UsageLogs from 'src/routes/UsageLogs/UsageLogs';
|
||||
|
||||
export enum OrganizationDrawerContentType {
|
||||
None,
|
||||
|
@ -50,7 +50,7 @@ export default function AutoPruning(props: AutoPruning) {
|
||||
error,
|
||||
isSuccess: successFetchingPolicies,
|
||||
isLoading,
|
||||
policies,
|
||||
nsPolicies,
|
||||
dataUpdatedAt,
|
||||
} = useNamespaceAutoPrunePolicies(props.org, props.isUser);
|
||||
const {
|
||||
@ -76,8 +76,8 @@ export default function AutoPruning(props: AutoPruning) {
|
||||
if (successFetchingPolicies) {
|
||||
// Currently we only support one policy per namespace but
|
||||
// this will change in the future.
|
||||
if (policies.length > 0) {
|
||||
const policy: NamespaceAutoPrunePolicy = policies[0];
|
||||
if (nsPolicies.length > 0) {
|
||||
const policy: NamespaceAutoPrunePolicy = nsPolicies[0];
|
||||
setMethod(policy.method);
|
||||
setUuid(policy.uuid);
|
||||
switch (policy.method) {
|
||||
|
@ -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;
|
||||
}
|
@ -8,10 +8,14 @@ import Visibility from './Visibility';
|
||||
import {RepositoryStateForm} from './RepositoryState';
|
||||
import {RepositoryDetails} from 'src/resources/RepositoryResource';
|
||||
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) {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
const config = useQuayConfig();
|
||||
const {isUserOrganization} = useOrganization(props.org);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
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',
|
||||
id: 'eventsandnotifications',
|
||||
|
@ -1,4 +1,6 @@
|
||||
import logging.config
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import features
|
||||
@ -35,21 +37,42 @@ class AutoPruneWorker(Worker):
|
||||
autoprune_task.id,
|
||||
autoprune_task.namespace,
|
||||
)
|
||||
|
||||
repo_policies = []
|
||||
try:
|
||||
policies = get_namespace_autoprune_policies_by_id(autoprune_task.namespace)
|
||||
if not policies:
|
||||
# When implementing repo policies, fetch repo policies before deleting the task
|
||||
delete_autoprune_task(autoprune_task)
|
||||
continue
|
||||
ns_policies = get_namespace_autoprune_policies_by_id(autoprune_task.namespace)
|
||||
if not ns_policies:
|
||||
repo_policies = get_repository_autoprune_policies_by_namespace_id(
|
||||
autoprune_task.namespace
|
||||
)
|
||||
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(
|
||||
policies,
|
||||
ns_policies,
|
||||
autoprune_task.namespace,
|
||||
FETCH_REPOSITORIES_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")
|
||||
except Exception as err:
|
||||
update_autoprune_task(autoprune_task, task_status=f"failure: {str(err)}")
|
||||
|
Reference in New Issue
Block a user