mirror of
https://github.com/quay/quay.git
synced 2025-07-31 18:44:32 +03:00
feat: Add auto-prune policy at repository level (PROJQUAY-6354) (#2691)
* feat: Add support for auto pruning at repository level (PROJQUAY-6354) * Add repositoryautoprunepolicy table to alembic migration script * Add repository auto-prune policy endpoints * Add UI for repository auto-pruning policies * case: apply repo auto-prune policy when no namespace policy given * case: both namespace and repo pruning policy are given * Add tests for repository autoprune endpoint * Add cypress test for repository auto-prune * Add repo auto-prune policy clean-up for repository deletion * Add repository auto pruning tables to quay db snapshot for cypress tests * Address review comments * Add more tests + fix CI + reformat files * Address review comments #2 --------- Signed-off-by: harishsurf <hgovinda@redhat.com>
This commit is contained in:
committed by
GitHub
parent
29258ae0c7
commit
98811f5397
@ -748,6 +748,7 @@ class User(BaseModel):
|
|||||||
OrganizationRhSkus,
|
OrganizationRhSkus,
|
||||||
NamespaceAutoPrunePolicy,
|
NamespaceAutoPrunePolicy,
|
||||||
AutoPruneTaskStatus,
|
AutoPruneTaskStatus,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
}
|
}
|
||||||
| appr_classes
|
| appr_classes
|
||||||
| v22_classes
|
| v22_classes
|
||||||
@ -968,6 +969,7 @@ class Repository(BaseModel):
|
|||||||
UploadedBlob,
|
UploadedBlob,
|
||||||
QuotaNamespaceSize,
|
QuotaNamespaceSize,
|
||||||
QuotaRepositorySize,
|
QuotaRepositorySize,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
}
|
}
|
||||||
| appr_classes
|
| appr_classes
|
||||||
| v22_classes
|
| v22_classes
|
||||||
@ -2021,6 +2023,13 @@ class AutoPruneTaskStatus(BaseModel):
|
|||||||
status = TextField(null=True)
|
status = TextField(null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryAutoPrunePolicy(BaseModel):
|
||||||
|
uuid = CharField(default=uuid_generator, max_length=36, index=True, null=False)
|
||||||
|
repository = ForeignKeyField(Repository, index=True, null=False)
|
||||||
|
namespace = QuayUserField(index=True, null=False)
|
||||||
|
policy = JSONField(null=False, default={})
|
||||||
|
|
||||||
|
|
||||||
# Defines a map from full-length index names to the legacy names used in our code
|
# Defines a map from full-length index names to the legacy names used in our code
|
||||||
# to meet length restrictions.
|
# to meet length restrictions.
|
||||||
LEGACY_INDEX_MAP = {
|
LEGACY_INDEX_MAP = {
|
||||||
|
@ -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
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryAutoPrunePolicyAlreadyExists(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryAutoPrunePolicyDoesNotExist(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRepositoryAutoPrunePolicy(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRepositoryAutoPruneMethod(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRepositoryException(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TooManyLoginAttemptsException(Exception):
|
class TooManyLoginAttemptsException(Exception):
|
||||||
def __init__(self, message, retry_after):
|
def __init__(self, message, retry_after):
|
||||||
super(TooManyLoginAttemptsException, self).__init__(message)
|
super(TooManyLoginAttemptsException, self).__init__(message)
|
||||||
|
@ -2,21 +2,21 @@ import json
|
|||||||
import logging.config
|
import logging.config
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from data.database import AutoPruneTaskStatus, DeletedNamespace
|
from data.database import AutoPruneTaskStatus
|
||||||
from data.database import NamespaceAutoPrunePolicy as NamespaceAutoPrunePolicyTable
|
from data.database import NamespaceAutoPrunePolicy as NamespaceAutoPrunePolicyTable
|
||||||
from data.database import (
|
from data.database import Repository
|
||||||
Repository,
|
from data.database import RepositoryAutoPrunePolicy as RepositoryAutoPrunePolicyTable
|
||||||
RepositoryState,
|
from data.database import RepositoryState, User, db_for_update, get_epoch_timestamp_ms
|
||||||
User,
|
|
||||||
db_for_update,
|
|
||||||
get_epoch_timestamp_ms,
|
|
||||||
)
|
|
||||||
from data.model import (
|
from data.model import (
|
||||||
InvalidNamespaceAutoPruneMethod,
|
InvalidNamespaceAutoPruneMethod,
|
||||||
InvalidNamespaceAutoPrunePolicy,
|
InvalidNamespaceAutoPrunePolicy,
|
||||||
InvalidNamespaceException,
|
InvalidNamespaceException,
|
||||||
|
InvalidRepositoryAutoPrunePolicy,
|
||||||
|
InvalidRepositoryException,
|
||||||
NamespaceAutoPrunePolicyAlreadyExists,
|
NamespaceAutoPrunePolicyAlreadyExists,
|
||||||
NamespaceAutoPrunePolicyDoesNotExist,
|
NamespaceAutoPrunePolicyDoesNotExist,
|
||||||
|
RepositoryAutoPrunePolicyAlreadyExists,
|
||||||
|
RepositoryAutoPrunePolicyDoesNotExist,
|
||||||
db_transaction,
|
db_transaction,
|
||||||
log,
|
log,
|
||||||
modelutil,
|
modelutil,
|
||||||
@ -53,6 +53,22 @@ class NamespaceAutoPrunePolicy:
|
|||||||
return {"uuid": self.uuid, "method": self.method, "value": self.config.get("value")}
|
return {"uuid": self.uuid, "method": self.method, "value": self.config.get("value")}
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryAutoPrunePolicy:
|
||||||
|
def __init__(self, db_row):
|
||||||
|
config = json.loads(db_row.policy)
|
||||||
|
self._db_row = db_row
|
||||||
|
self.uuid = db_row.uuid
|
||||||
|
self.method = config.get("method")
|
||||||
|
self.config = config
|
||||||
|
self.repository_id = db_row.repository_id
|
||||||
|
|
||||||
|
def get_row(self):
|
||||||
|
return self._db_row
|
||||||
|
|
||||||
|
def get_view(self):
|
||||||
|
return {"uuid": self.uuid, "method": self.method, "value": self.config.get("value")}
|
||||||
|
|
||||||
|
|
||||||
def valid_value(method, value):
|
def valid_value(method, value):
|
||||||
"""
|
"""
|
||||||
Method for validating the value provided for the policy method.
|
Method for validating the value provided for the policy method.
|
||||||
@ -88,6 +104,19 @@ def assert_valid_namespace_autoprune_policy(policy_config):
|
|||||||
raise InvalidNamespaceAutoPrunePolicy("Invalid value given for method type")
|
raise InvalidNamespaceAutoPrunePolicy("Invalid value given for method type")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_valid_repository_autoprune_policy(policy_config):
|
||||||
|
"""
|
||||||
|
Asserts that the policy config is valid.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
method = AutoPruneMethod(policy_config.get("method"))
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidRepositoryAutoPrunePolicy("Invalid method provided")
|
||||||
|
|
||||||
|
if not valid_value(method, policy_config.get("value")):
|
||||||
|
raise InvalidRepositoryAutoPrunePolicy("Invalid value given for method type")
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_autoprune_policies_by_orgname(orgname):
|
def get_namespace_autoprune_policies_by_orgname(orgname):
|
||||||
"""
|
"""
|
||||||
Get the autopruning policies for the specified namespace.
|
Get the autopruning policies for the specified namespace.
|
||||||
@ -102,6 +131,34 @@ def get_namespace_autoprune_policies_by_orgname(orgname):
|
|||||||
return [NamespaceAutoPrunePolicy(row) for row in query]
|
return [NamespaceAutoPrunePolicy(row) for row in query]
|
||||||
|
|
||||||
|
|
||||||
|
def get_repository_autoprune_policies_by_repo_name(orgname, repo_name):
|
||||||
|
"""
|
||||||
|
Get the autopruning policies for the specified repository.
|
||||||
|
"""
|
||||||
|
query = (
|
||||||
|
RepositoryAutoPrunePolicyTable.select(RepositoryAutoPrunePolicyTable)
|
||||||
|
.join(Repository)
|
||||||
|
.join(User)
|
||||||
|
.where(
|
||||||
|
User.username == orgname,
|
||||||
|
RepositoryAutoPrunePolicyTable.repository == Repository.id,
|
||||||
|
Repository.name == repo_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return [RepositoryAutoPrunePolicy(row) for row in query]
|
||||||
|
|
||||||
|
|
||||||
|
def get_repository_autoprune_policies_by_repo_id(repo_id):
|
||||||
|
"""
|
||||||
|
Get the autopruning policies for the specified repository.
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = RepositoryAutoPrunePolicyTable.select().where(
|
||||||
|
RepositoryAutoPrunePolicyTable.repository == repo_id,
|
||||||
|
)
|
||||||
|
return [RepositoryAutoPrunePolicy(row) for row in query]
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_autoprune_policies_by_id(namespace_id):
|
def get_namespace_autoprune_policies_by_id(namespace_id):
|
||||||
"""
|
"""
|
||||||
Get the autopruning policies for the namespace by id.
|
Get the autopruning policies for the namespace by id.
|
||||||
@ -112,6 +169,16 @@ def get_namespace_autoprune_policies_by_id(namespace_id):
|
|||||||
return [NamespaceAutoPrunePolicy(row) for row in query]
|
return [NamespaceAutoPrunePolicy(row) for row in query]
|
||||||
|
|
||||||
|
|
||||||
|
def get_repository_autoprune_policies_by_namespace_id(namespace_id):
|
||||||
|
"""
|
||||||
|
Get all repository autopruning policies for a namespace by id.
|
||||||
|
"""
|
||||||
|
query = RepositoryAutoPrunePolicyTable.select().where(
|
||||||
|
RepositoryAutoPrunePolicyTable.namespace == namespace_id,
|
||||||
|
)
|
||||||
|
return [RepositoryAutoPrunePolicy(row) for row in query]
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_autoprune_policy(orgname, uuid):
|
def get_namespace_autoprune_policy(orgname, uuid):
|
||||||
"""
|
"""
|
||||||
Get the specific autopruning policy for the specified namespace by uuid.
|
Get the specific autopruning policy for the specified namespace by uuid.
|
||||||
@ -128,6 +195,22 @@ def get_namespace_autoprune_policy(orgname, uuid):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_repository_autoprune_policy_by_uuid(repo_name, uuid):
|
||||||
|
"""
|
||||||
|
Get the specific autopruning policy for the specified repository by uuid.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
row = (
|
||||||
|
RepositoryAutoPrunePolicyTable.select(RepositoryAutoPrunePolicyTable)
|
||||||
|
.join(Repository)
|
||||||
|
.where(Repository.name == repo_name, RepositoryAutoPrunePolicyTable.uuid == uuid)
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
return RepositoryAutoPrunePolicy(row)
|
||||||
|
except RepositoryAutoPrunePolicyTable.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def create_namespace_autoprune_policy(orgname, policy_config, create_task=False):
|
def create_namespace_autoprune_policy(orgname, policy_config, create_task=False):
|
||||||
"""
|
"""
|
||||||
Creates the namespace auto-prune policy. If create_task is True, then it will also create
|
Creates the namespace auto-prune policy. If create_task is True, then it will also create
|
||||||
@ -156,6 +239,37 @@ def create_namespace_autoprune_policy(orgname, policy_config, create_task=False)
|
|||||||
return new_policy
|
return new_policy
|
||||||
|
|
||||||
|
|
||||||
|
def create_repository_autoprune_policy(orgname, repo_name, policy_config, create_task=False):
|
||||||
|
"""
|
||||||
|
Creates the repository auto-prune policy. If create_task is True, it will check if auto-prune task is not already present,
|
||||||
|
and only then it will create the auto-prune task. Deletion of the task will be handled by the autoprune worker.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with db_transaction():
|
||||||
|
namespace = get_active_namespace_user_by_username(orgname)
|
||||||
|
namespace_id = namespace.id
|
||||||
|
|
||||||
|
repo = repository.get_repository(orgname, repo_name)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise InvalidRepositoryException("Repository does not exist: %s" % repo_name)
|
||||||
|
|
||||||
|
if repository_has_autoprune_policy(repo.id):
|
||||||
|
raise RepositoryAutoPrunePolicyAlreadyExists(
|
||||||
|
"Policy for this repository already exists, delete existing to create new policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_valid_repository_autoprune_policy(policy_config)
|
||||||
|
|
||||||
|
new_policy = RepositoryAutoPrunePolicyTable.create(
|
||||||
|
namespace=namespace_id, repository=repo.id, policy=json.dumps(policy_config)
|
||||||
|
)
|
||||||
|
if create_task and not namespace_has_autoprune_task(namespace_id):
|
||||||
|
create_autoprune_task(namespace_id)
|
||||||
|
|
||||||
|
return new_policy
|
||||||
|
|
||||||
|
|
||||||
def update_namespace_autoprune_policy(orgname, uuid, policy_config):
|
def update_namespace_autoprune_policy(orgname, uuid, policy_config):
|
||||||
"""
|
"""
|
||||||
Updates the namespace auto-prune policy with the provided policy config
|
Updates the namespace auto-prune policy with the provided policy config
|
||||||
@ -184,9 +298,41 @@ def update_namespace_autoprune_policy(orgname, uuid, policy_config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def update_repository_autoprune_policy(orgname, repo_name, uuid, policy_config):
|
||||||
|
"""
|
||||||
|
Updates the repository auto-prune policy with the provided policy config
|
||||||
|
for the specified uuid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
namespace = get_active_namespace_user_by_username(orgname)
|
||||||
|
namespace_id = namespace.id
|
||||||
|
|
||||||
|
repo = repository.get_repository(orgname, repo_name)
|
||||||
|
if repo is None:
|
||||||
|
raise InvalidRepositoryException("Repository does not exist: %s" % repo_name)
|
||||||
|
|
||||||
|
policy = get_repository_autoprune_policy_by_uuid(repo_name, uuid)
|
||||||
|
if policy is None:
|
||||||
|
raise RepositoryAutoPrunePolicyDoesNotExist(
|
||||||
|
f"Policy not found for repository: {repo_name} with uuid: {uuid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_valid_repository_autoprune_policy(policy_config)
|
||||||
|
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicyTable.update(policy=json.dumps(policy_config))
|
||||||
|
.where(
|
||||||
|
RepositoryAutoPrunePolicyTable.uuid == uuid,
|
||||||
|
RepositoryAutoPrunePolicyTable.namespace == namespace_id,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def delete_namespace_autoprune_policy(orgname, uuid):
|
def delete_namespace_autoprune_policy(orgname, uuid):
|
||||||
"""
|
"""
|
||||||
Deletes the policy specified by the uuid
|
Deletes the namespace policy specified by the uuid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with db_transaction():
|
with db_transaction():
|
||||||
@ -217,6 +363,43 @@ def delete_namespace_autoprune_policy(orgname, uuid):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def delete_repository_autoprune_policy(orgname, repo_name, uuid):
|
||||||
|
"""
|
||||||
|
Deletes the repository policy specified by the uuid
|
||||||
|
"""
|
||||||
|
|
||||||
|
with db_transaction():
|
||||||
|
try:
|
||||||
|
namespace_id = User.select().where(User.username == orgname).get().id
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise InvalidNamespaceException("Invalid namespace provided: %s" % (orgname))
|
||||||
|
|
||||||
|
repo = repository.get_repository(orgname, repo_name)
|
||||||
|
if repo is None:
|
||||||
|
raise InvalidRepositoryException("Repository does not exist: %s" % repo_name)
|
||||||
|
|
||||||
|
policy = get_repository_autoprune_policy_by_uuid(repo_name, uuid)
|
||||||
|
if policy is None:
|
||||||
|
raise RepositoryAutoPrunePolicyDoesNotExist(
|
||||||
|
f"Policy not found for repository: {repo_name} with uuid: {uuid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = (
|
||||||
|
RepositoryAutoPrunePolicyTable.delete()
|
||||||
|
.where(
|
||||||
|
RepositoryAutoPrunePolicyTable.uuid == uuid,
|
||||||
|
RepositoryAutoPrunePolicyTable.namespace == namespace_id,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
raise RepositoryAutoPrunePolicyTable.DoesNotExist(
|
||||||
|
f"Policy not found for repository: {repo_name} with uuid: {uuid}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def namespace_has_autoprune_policy(namespace_id):
|
def namespace_has_autoprune_policy(namespace_id):
|
||||||
return (
|
return (
|
||||||
NamespaceAutoPrunePolicyTable.select(1)
|
NamespaceAutoPrunePolicyTable.select(1)
|
||||||
@ -225,6 +408,14 @@ def namespace_has_autoprune_policy(namespace_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def repository_has_autoprune_policy(repository_id):
|
||||||
|
return (
|
||||||
|
RepositoryAutoPrunePolicyTable.select(1)
|
||||||
|
.where(RepositoryAutoPrunePolicyTable.repository == repository_id)
|
||||||
|
.exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def namespace_has_autoprune_task(namespace_id):
|
def namespace_has_autoprune_task(namespace_id):
|
||||||
return (
|
return (
|
||||||
AutoPruneTaskStatus.select(1).where(AutoPruneTaskStatus.namespace == namespace_id).exists()
|
AutoPruneTaskStatus.select(1).where(AutoPruneTaskStatus.namespace == namespace_id).exists()
|
||||||
@ -260,6 +451,7 @@ def fetch_autoprune_task(task_run_interval_ms=60 * 60 * 1000):
|
|||||||
AutoPruneTaskStatus.select(AutoPruneTaskStatus)
|
AutoPruneTaskStatus.select(AutoPruneTaskStatus)
|
||||||
.where(
|
.where(
|
||||||
AutoPruneTaskStatus.namespace.not_in(
|
AutoPruneTaskStatus.namespace.not_in(
|
||||||
|
# this basically skips ns if user is not enabled
|
||||||
User.select(User.id).where(
|
User.select(User.id).where(
|
||||||
User.enabled == False, User.id == AutoPruneTaskStatus.namespace
|
User.enabled == False, User.id == AutoPruneTaskStatus.namespace
|
||||||
)
|
)
|
||||||
@ -411,12 +603,18 @@ def execute_policy_on_repo(policy, repo_id, namespace_id, tag_page_limit=100):
|
|||||||
policy_to_func_map[policy.method](repo, policy.config, namespace, tag_page_limit)
|
policy_to_func_map[policy.method](repo, policy.config, namespace, tag_page_limit)
|
||||||
|
|
||||||
|
|
||||||
def execute_policies_for_repo(policies, repo, namespace_id, tag_page_limit=100):
|
def execute_policies_for_repo(ns_policies, repo, namespace_id, tag_page_limit=100):
|
||||||
"""
|
"""
|
||||||
Executes the policies for the given repository.
|
Executes both repository and namespace level policies for the given repository. The policies
|
||||||
|
are applied in a serial fashion and are run asynchronosly in the background.
|
||||||
"""
|
"""
|
||||||
for policy in policies:
|
for ns_policy in ns_policies:
|
||||||
execute_policy_on_repo(policy, repo, namespace_id, tag_page_limit)
|
repo_policies = get_repository_autoprune_policies_by_repo_id(repo.id)
|
||||||
|
# note: currently only one policy is configured per repo
|
||||||
|
for repo_policy in repo_policies:
|
||||||
|
execute_policy_on_repo(repo_policy, repo.id, namespace_id, tag_page_limit)
|
||||||
|
# execute associated namespace policy
|
||||||
|
execute_policy_on_repo(ns_policy, repo.id, namespace_id, tag_page_limit)
|
||||||
|
|
||||||
|
|
||||||
def get_paginated_repositories_for_namespace(namespace_id, page_token=None, page_size=50):
|
def get_paginated_repositories_for_namespace(namespace_id, page_token=None, page_size=50):
|
||||||
@ -438,12 +636,28 @@ def get_paginated_repositories_for_namespace(namespace_id, page_token=None, page
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def execute_namespace_polices(policies, namespace_id, repository_page_limit=50, tag_page_limit=100):
|
def get_repository_by_policy_repo_id(policy_repo_id):
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
Repository.select(Repository.name)
|
||||||
|
.where(
|
||||||
|
Repository.id == policy_repo_id,
|
||||||
|
Repository.state == RepositoryState.NORMAL,
|
||||||
|
)
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
except Repository.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def execute_namespace_polices(
|
||||||
|
ns_policies, namespace_id, repository_page_limit=50, tag_page_limit=100
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Executes the given policies for the repositories in the provided namespace.
|
Executes the given policies for the repositories in the provided namespace.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not policies:
|
if not ns_policies:
|
||||||
return
|
return
|
||||||
page_token = None
|
page_token = None
|
||||||
|
|
||||||
@ -453,7 +667,7 @@ def execute_namespace_polices(policies, namespace_id, repository_page_limit=50,
|
|||||||
)
|
)
|
||||||
|
|
||||||
for repo in repos:
|
for repo in repos:
|
||||||
execute_policies_for_repo(policies, repo, namespace_id, tag_page_limit)
|
execute_policies_for_repo(ns_policies, repo, namespace_id, tag_page_limit)
|
||||||
|
|
||||||
if page_token is None:
|
if page_token is None:
|
||||||
break
|
break
|
||||||
|
@ -21,6 +21,7 @@ from data.database import (
|
|||||||
Repository,
|
Repository,
|
||||||
RepositoryActionCount,
|
RepositoryActionCount,
|
||||||
RepositoryAuthorizedEmail,
|
RepositoryAuthorizedEmail,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
RepositoryBuild,
|
RepositoryBuild,
|
||||||
RepositoryBuildTrigger,
|
RepositoryBuildTrigger,
|
||||||
RepositoryNotification,
|
RepositoryNotification,
|
||||||
@ -105,6 +106,8 @@ def purge_repository(repo, force=False):
|
|||||||
ManifestSecurityStatus.select().where(ManifestSecurityStatus.repository == repo).count()
|
ManifestSecurityStatus.select().where(ManifestSecurityStatus.repository == repo).count()
|
||||||
== 0
|
== 0
|
||||||
)
|
)
|
||||||
|
# Delete auto-prune policy associated with the repository
|
||||||
|
RepositoryAutoPrunePolicy.delete().where(RepositoryAutoPrunePolicy.repository == repo).execute()
|
||||||
|
|
||||||
# Delete any repository build triggers, builds, and any other large-ish reference tables for
|
# Delete any repository build triggers, builds, and any other large-ish reference tables for
|
||||||
# the repository.
|
# the repository.
|
||||||
|
@ -52,9 +52,9 @@ def test_load_security_information(indexed_v4, expected_status, initialized_db):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"next_token, expected_next_token, expected_error",
|
"next_token, expected_next_token, expected_error",
|
||||||
[
|
[
|
||||||
(None, V4ScanToken(56), None),
|
(None, V4ScanToken(58), None),
|
||||||
(V4ScanToken(None), V4ScanToken(56), AssertionError),
|
(V4ScanToken(None), V4ScanToken(58), AssertionError),
|
||||||
(V4ScanToken(1), V4ScanToken(56), None),
|
(V4ScanToken(1), V4ScanToken(58), None),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_perform_indexing(next_token, expected_next_token, expected_error, initialized_db):
|
def test_perform_indexing(next_token, expected_next_token, expected_error, initialized_db):
|
||||||
|
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
|
import features
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth.permissions import AdministerOrganizationPermission
|
from auth.permissions import (
|
||||||
|
AdministerOrganizationPermission,
|
||||||
|
AdministerRepositoryPermission,
|
||||||
|
)
|
||||||
from data import model
|
from data import model
|
||||||
|
from data.registry_model import registry_model
|
||||||
from endpoints.api import (
|
from endpoints.api import (
|
||||||
ApiResource,
|
ApiResource,
|
||||||
|
RepositoryParamResource,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
log_action,
|
log_action,
|
||||||
nickname,
|
nickname,
|
||||||
path_param,
|
path_param,
|
||||||
request_error,
|
request_error,
|
||||||
|
require_repo_admin,
|
||||||
require_scope,
|
require_scope,
|
||||||
require_user_admin,
|
require_user_admin,
|
||||||
resource,
|
resource,
|
||||||
@ -231,6 +237,233 @@ class OrgAutoPrunePolicy(ApiResource):
|
|||||||
return {"uuid": policy_uuid}, 200
|
return {"uuid": policy_uuid}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@resource("/v1/repository/<apirepopath:repository>/autoprunepolicy/")
|
||||||
|
@path_param("repository", "The full path of the repository. e.g. namespace/name")
|
||||||
|
@show_if(features.AUTO_PRUNE)
|
||||||
|
class RepositoryAutoPrunePolicies(RepositoryParamResource):
|
||||||
|
"""
|
||||||
|
Resource for listing and creating repository auto-prune policies
|
||||||
|
"""
|
||||||
|
|
||||||
|
schemas = {
|
||||||
|
"AutoPrunePolicyConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "The policy configuration that is to be applied to the repository",
|
||||||
|
"required": ["method", "value"],
|
||||||
|
"properties": {
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The method to use for pruning tags (number_of_tags, creation_date)",
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": ["integer", "string"],
|
||||||
|
"description": "The value to use for the pruning method (number of tags e.g. 10, time delta e.g. 7d (7 days))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_repo_admin(allow_for_superuser=True)
|
||||||
|
@nickname("listRepositoryAutoPrunePolicies")
|
||||||
|
def get(self, namespace, repository):
|
||||||
|
"""
|
||||||
|
Lists the auto-prune policies for the repository
|
||||||
|
"""
|
||||||
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
|
if not permission.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
if registry_model.lookup_repository(namespace, repository) is None:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
policies = model.autoprune.get_repository_autoprune_policies_by_repo_name(
|
||||||
|
namespace, repository
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"policies": [policy.get_view() for policy in policies]}
|
||||||
|
|
||||||
|
@require_repo_admin(allow_for_superuser=True)
|
||||||
|
@validate_json_request("AutoPrunePolicyConfig")
|
||||||
|
@nickname("createRepositoryAutoPrunePolicy")
|
||||||
|
def post(self, namespace, repository):
|
||||||
|
"""
|
||||||
|
Creates an auto-prune policy for the repository
|
||||||
|
"""
|
||||||
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
|
if not permission.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
if registry_model.lookup_repository(namespace, repository) is None:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
app_data = request.get_json()
|
||||||
|
method = app_data.get("method", None)
|
||||||
|
value = app_data.get("value", None)
|
||||||
|
|
||||||
|
if method is None or value is None:
|
||||||
|
request_error(message="Missing the following parameters: method, value")
|
||||||
|
|
||||||
|
policy_config = {
|
||||||
|
"method": method,
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
policy = model.autoprune.create_repository_autoprune_policy(
|
||||||
|
namespace, repository, policy_config, create_task=True
|
||||||
|
)
|
||||||
|
except model.InvalidNamespaceException:
|
||||||
|
raise NotFound()
|
||||||
|
except model.InvalidRepositoryException:
|
||||||
|
raise NotFound()
|
||||||
|
except model.InvalidRepositoryAutoPrunePolicy as ex:
|
||||||
|
request_error(ex)
|
||||||
|
except model.RepositoryAutoPrunePolicyAlreadyExists as ex:
|
||||||
|
request_error(ex)
|
||||||
|
|
||||||
|
log_action(
|
||||||
|
"create_repository_autoprune_policy",
|
||||||
|
namespace,
|
||||||
|
{
|
||||||
|
"method": policy_config["method"],
|
||||||
|
"value": policy_config["value"],
|
||||||
|
"namespace": namespace,
|
||||||
|
"repo": repository,
|
||||||
|
},
|
||||||
|
repo_name=repository,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"uuid": policy.uuid}, 201
|
||||||
|
|
||||||
|
|
||||||
|
@resource("/v1/repository/<apirepopath:repository>/autoprunepolicy/<policy_uuid>")
|
||||||
|
@path_param("repository", "The full path of the repository. e.g. namespace/name")
|
||||||
|
@path_param("policy_uuid", "The unique ID of the policy")
|
||||||
|
@show_if(features.AUTO_PRUNE)
|
||||||
|
class RepositoryAutoPrunePolicy(RepositoryParamResource):
|
||||||
|
"""
|
||||||
|
Resource for fetching, updating, and deleting repository specific auto-prune policies
|
||||||
|
"""
|
||||||
|
|
||||||
|
schemas = {
|
||||||
|
"AutoPrunePolicyConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "The policy configuration that is to be applied to the repository",
|
||||||
|
"required": ["method", "value"],
|
||||||
|
"properties": {
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The method to use for pruning tags (number_of_tags, creation_date)",
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": ["integer", "string"],
|
||||||
|
"description": "The value to use for the pruning method (number of tags e.g. 10, time delta e.g. 7d (7 days))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_repo_admin(allow_for_superuser=True)
|
||||||
|
@nickname("getRepositoryAutoPrunePolicy")
|
||||||
|
def get(self, namespace, repository, policy_uuid):
|
||||||
|
"""
|
||||||
|
Fetches the auto-prune policy for the repository
|
||||||
|
"""
|
||||||
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
|
if not permission.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
policy = model.autoprune.get_repository_autoprune_policy_by_uuid(repository, policy_uuid)
|
||||||
|
if policy is None:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
return policy.get_view()
|
||||||
|
|
||||||
|
@require_repo_admin(allow_for_superuser=True)
|
||||||
|
@validate_json_request("AutoPrunePolicyConfig")
|
||||||
|
@nickname("updateRepositoryAutoPrunePolicy")
|
||||||
|
def put(self, namespace, repository, policy_uuid):
|
||||||
|
"""
|
||||||
|
Updates the auto-prune policy for the repository
|
||||||
|
"""
|
||||||
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
|
if not permission.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
app_data = request.get_json()
|
||||||
|
method = app_data.get("method", None)
|
||||||
|
value = app_data.get("value", None)
|
||||||
|
|
||||||
|
if method is None or value is None:
|
||||||
|
request_error(message="Missing the following parameters: method, value")
|
||||||
|
|
||||||
|
policy_config = {
|
||||||
|
"method": method,
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated = model.autoprune.update_repository_autoprune_policy(
|
||||||
|
namespace, repository, policy_uuid, policy_config
|
||||||
|
)
|
||||||
|
if not updated:
|
||||||
|
request_error(message="could not update policy")
|
||||||
|
except model.InvalidNamespaceException:
|
||||||
|
raise NotFound()
|
||||||
|
except model.InvalidRepositoryException:
|
||||||
|
raise NotFound()
|
||||||
|
except model.InvalidRepositoryAutoPrunePolicy as ex:
|
||||||
|
request_error(ex)
|
||||||
|
except model.RepositoryAutoPrunePolicyDoesNotExist as ex:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
log_action(
|
||||||
|
"update_repository_autoprune_policy",
|
||||||
|
namespace,
|
||||||
|
{
|
||||||
|
"method": policy_config["method"],
|
||||||
|
"value": policy_config["value"],
|
||||||
|
"namespace": namespace,
|
||||||
|
"repo": repository,
|
||||||
|
},
|
||||||
|
repo_name=repository,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"uuid": policy_uuid}, 204
|
||||||
|
|
||||||
|
@require_repo_admin(allow_for_superuser=True)
|
||||||
|
@nickname("deleteRepositoryAutoPrunePolicy")
|
||||||
|
def delete(self, namespace, repository, policy_uuid):
|
||||||
|
"""
|
||||||
|
Deletes the auto-prune policy for the repository
|
||||||
|
"""
|
||||||
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
|
if not permission.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated = model.autoprune.delete_repository_autoprune_policy(
|
||||||
|
namespace, repository, policy_uuid
|
||||||
|
)
|
||||||
|
if not updated:
|
||||||
|
raise InvalidRequest("could not delete policy")
|
||||||
|
except model.InvalidNamespaceException:
|
||||||
|
raise NotFound()
|
||||||
|
except model.InvalidRepositoryException as ex:
|
||||||
|
raise NotFound()
|
||||||
|
except model.RepositoryAutoPrunePolicyDoesNotExist as ex:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
log_action(
|
||||||
|
"delete_repository_autoprune_policy",
|
||||||
|
namespace,
|
||||||
|
{"policy_uuid": policy_uuid, "namespace": namespace, "repo": repository},
|
||||||
|
repo_name=repository,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"uuid": policy_uuid}, 200
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/user/autoprunepolicy/")
|
@resource("/v1/user/autoprunepolicy/")
|
||||||
@show_if(features.AUTO_PRUNE)
|
@show_if(features.AUTO_PRUNE)
|
||||||
class UserAutoPrunePolicies(ApiResource):
|
class UserAutoPrunePolicies(ApiResource):
|
||||||
|
@ -7,6 +7,8 @@ from data.model.log import get_latest_logs_query, get_log_entry_kinds
|
|||||||
from endpoints.api.policy import (
|
from endpoints.api.policy import (
|
||||||
OrgAutoPrunePolicies,
|
OrgAutoPrunePolicies,
|
||||||
OrgAutoPrunePolicy,
|
OrgAutoPrunePolicy,
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
UserAutoPrunePolicies,
|
UserAutoPrunePolicies,
|
||||||
UserAutoPrunePolicy,
|
UserAutoPrunePolicy,
|
||||||
)
|
)
|
||||||
@ -345,3 +347,187 @@ def test_delete_user_policy_nonexistent_policy(initialized_db, app):
|
|||||||
{"orgname": "devtable", "policy_uuid": "doesnotexist"},
|
{"orgname": "devtable", "policy_uuid": "doesnotexist"},
|
||||||
expected_code=404,
|
expected_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_repo_policies(initialized_db, app):
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params = {"repository": "devtable/simple"}
|
||||||
|
response = conduct_api_call(cl, RepositoryAutoPrunePolicies, "GET", params).json
|
||||||
|
assert len(response["policies"]) == 1
|
||||||
|
assert response["policies"][0]["method"] == "number_of_tags"
|
||||||
|
assert response["policies"][0]["value"] == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_repo_policy(initialized_db, app):
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params = {"repository": "testorgforautoprune/autoprunerepo"}
|
||||||
|
response = conduct_api_call(
|
||||||
|
cl,
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"POST",
|
||||||
|
params,
|
||||||
|
{"method": "creation_date", "value": "2w"},
|
||||||
|
201,
|
||||||
|
).json
|
||||||
|
assert response["uuid"] is not None
|
||||||
|
assert (
|
||||||
|
model.autoprune.get_repository_autoprune_policy_by_uuid(
|
||||||
|
"autoprunerepo", response["uuid"]
|
||||||
|
)
|
||||||
|
is not None
|
||||||
|
)
|
||||||
|
org = model.organization.get_organization("testorgforautoprune")
|
||||||
|
assert model.autoprune.namespace_has_autoprune_task(org.id)
|
||||||
|
|
||||||
|
# Check audit log was created
|
||||||
|
logs = list(get_latest_logs_query(namespace="testorgforautoprune"))
|
||||||
|
log_kinds = get_log_entry_kinds()
|
||||||
|
log = None
|
||||||
|
for l in logs:
|
||||||
|
if l.kind == log_kinds["create_repository_autoprune_policy"]:
|
||||||
|
log = l
|
||||||
|
break
|
||||||
|
assert log is not None
|
||||||
|
assert json.loads(log.metadata_json)["method"] == "creation_date"
|
||||||
|
assert json.loads(log.metadata_json)["value"] == "2w"
|
||||||
|
assert json.loads(log.metadata_json)["namespace"] == "testorgforautoprune"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_repo_policy_already_existing(initialized_db, app):
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params = {"repository": "devtable/simple"}
|
||||||
|
response = conduct_api_call(
|
||||||
|
cl,
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"POST",
|
||||||
|
params,
|
||||||
|
{"method": "creation_date", "value": "2w"},
|
||||||
|
expected_code=400,
|
||||||
|
).json
|
||||||
|
assert (
|
||||||
|
response["error_message"]
|
||||||
|
== "Policy for this repository already exists, delete existing to create new policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_repo_policy_nonexistent_method(initialized_db, app):
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params = {"repository": "testorgforautoprune/autoprunerepo"}
|
||||||
|
response = conduct_api_call(
|
||||||
|
cl,
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"POST",
|
||||||
|
params,
|
||||||
|
{"method": "doesnotexist", "value": "2w"},
|
||||||
|
expected_code=400,
|
||||||
|
).json
|
||||||
|
assert response["error_message"] == "Invalid method provided"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_repo_policy(initialized_db, app):
|
||||||
|
policies = model.autoprune.get_repository_autoprune_policies_by_repo_name("devtable", "simple")
|
||||||
|
assert len(policies) == 1
|
||||||
|
policy_uuid = policies[0].uuid
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
|
||||||
|
response = conduct_api_call(cl, RepositoryAutoPrunePolicy, "GET", params).json
|
||||||
|
assert response["method"] == "number_of_tags"
|
||||||
|
assert response["value"] == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_repo_policy(initialized_db, app):
|
||||||
|
policies = model.autoprune.get_repository_autoprune_policies_by_repo_name("devtable", "simple")
|
||||||
|
assert len(policies) == 1
|
||||||
|
policy_uuid = policies[0].uuid
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params_for_update = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
|
||||||
|
conduct_api_call(
|
||||||
|
cl,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"PUT",
|
||||||
|
params_for_update,
|
||||||
|
{"method": "creation_date", "value": "2w"},
|
||||||
|
expected_code=204,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make another request asserting it was updated
|
||||||
|
params = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
|
||||||
|
get_response = conduct_api_call(cl, RepositoryAutoPrunePolicy, "GET", params).json
|
||||||
|
assert get_response["method"] == "creation_date"
|
||||||
|
assert get_response["value"] == "2w"
|
||||||
|
|
||||||
|
# Check audit log was created
|
||||||
|
logs = list(get_latest_logs_query(namespace="devtable"))
|
||||||
|
log_kinds = get_log_entry_kinds()
|
||||||
|
log = None
|
||||||
|
for l in logs:
|
||||||
|
if l.kind == log_kinds["update_repository_autoprune_policy"]:
|
||||||
|
log = l
|
||||||
|
break
|
||||||
|
assert log is not None
|
||||||
|
assert json.loads(log.metadata_json)["method"] == "creation_date"
|
||||||
|
assert json.loads(log.metadata_json)["value"] == "2w"
|
||||||
|
assert json.loads(log.metadata_json)["namespace"] == "devtable"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_repo_policy_nonexistent_policy(initialized_db, app):
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params_for_update = {"repository": "devtable/simple", "policy_uuid": "doesnotexist"}
|
||||||
|
conduct_api_call(
|
||||||
|
cl,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"PUT",
|
||||||
|
params_for_update,
|
||||||
|
{"method": "creation_date", "value": "2w"},
|
||||||
|
expected_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_repo_policy(initialized_db, app):
|
||||||
|
policies = model.autoprune.get_repository_autoprune_policies_by_repo_name("devtable", "simple")
|
||||||
|
assert len(policies) == 1
|
||||||
|
policy_uuid = policies[0].uuid
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params_for_delete = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
|
||||||
|
conduct_api_call(
|
||||||
|
cl,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"DELETE",
|
||||||
|
params_for_delete,
|
||||||
|
expected_code=200,
|
||||||
|
)
|
||||||
|
params = {"repository": "devtable/simple", "policy_uuid": policy_uuid}
|
||||||
|
conduct_api_call(
|
||||||
|
cl,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"GET",
|
||||||
|
params,
|
||||||
|
expected_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check audit log was created
|
||||||
|
logs = list(get_latest_logs_query(namespace="devtable"))
|
||||||
|
log_kinds = get_log_entry_kinds()
|
||||||
|
log = None
|
||||||
|
for l in logs:
|
||||||
|
if l.kind == log_kinds["delete_repository_autoprune_policy"]:
|
||||||
|
log = l
|
||||||
|
break
|
||||||
|
assert log is not None
|
||||||
|
assert json.loads(log.metadata_json)["policy_uuid"] == policy_uuid
|
||||||
|
assert json.loads(log.metadata_json)["namespace"] == "devtable"
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_repo_policy_nonexistent_policy(initialized_db, app):
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
params_for_delete = {
|
||||||
|
"repository": "testorgforautoprune/autoprunerepo",
|
||||||
|
"policy_uuid": "doesnotexist",
|
||||||
|
}
|
||||||
|
conduct_api_call(
|
||||||
|
cl,
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"DELETE",
|
||||||
|
params_for_delete,
|
||||||
|
expected_code=404,
|
||||||
|
)
|
||||||
|
@ -6270,6 +6270,182 @@ SECURITY_TESTS: List[
|
|||||||
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "devtable", 404),
|
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "devtable", 404),
|
||||||
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "freshuser", 404),
|
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "freshuser", 404),
|
||||||
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "reader", 404),
|
(UserAutoPrunePolicy, "DELETE", {"policy_uuid": "some_uuid"}, None, "reader", 404),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo"},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/unknown"},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
404,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo"},
|
||||||
|
None,
|
||||||
|
"freshuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo"},
|
||||||
|
None,
|
||||||
|
"reader",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"POST",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo"},
|
||||||
|
{"method": "number_of_tags", "value": 10},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"POST",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo"},
|
||||||
|
{"method": "creation_date", "value": "2w"},
|
||||||
|
"devtable",
|
||||||
|
201,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"POST",
|
||||||
|
{"repository": "testorgforautoprune/unknown"},
|
||||||
|
{"method": "number_of_tags", "value": 10},
|
||||||
|
"devtable",
|
||||||
|
404,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"POST",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo"},
|
||||||
|
{"method": "number_of_tags", "value": 10},
|
||||||
|
"freshuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicies,
|
||||||
|
"POST",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo"},
|
||||||
|
{"method": "number_of_tags", "value": 10},
|
||||||
|
"reader",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/unknown", "policy_uuid": "some_uuid"},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
404,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
None,
|
||||||
|
"freshuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"GET",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
None,
|
||||||
|
"reader",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"PUT",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
{"method": "number_of_tags", "value": 10},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"PUT",
|
||||||
|
{"repository": "testorgforautoprune/unknown", "policy_uuid": "some_uuid"},
|
||||||
|
{"method": "number_of_tags", "value": 10},
|
||||||
|
"devtable",
|
||||||
|
404,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"PUT",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
{"method": "number_of_tags", "value": 10},
|
||||||
|
"freshuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"PUT",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
{"method": "number_of_tags", "value": 10},
|
||||||
|
"reader",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"DELETE",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"DELETE",
|
||||||
|
{"repository": "testorgforautoprune/unknown", "policy_uuid": "some_uuid"},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
404,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"DELETE",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
None,
|
||||||
|
"freshuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
RepositoryAutoPrunePolicy,
|
||||||
|
"DELETE",
|
||||||
|
{"repository": "testorgforautoprune/autoprunerepo", "policy_uuid": "some_uuid"},
|
||||||
|
None,
|
||||||
|
"reader",
|
||||||
|
403,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,14 +31,14 @@ def test_list_all_users(disabled, app):
|
|||||||
def test_list_all_orgs(app):
|
def test_list_all_orgs(app):
|
||||||
with client_with_identity("devtable", app) as cl:
|
with client_with_identity("devtable", app) as cl:
|
||||||
result = conduct_api_call(cl, SuperUserOrganizationList, "GET", None, None, 200).json
|
result = conduct_api_call(cl, SuperUserOrganizationList, "GET", None, None, 200).json
|
||||||
assert len(result["organizations"]) == 6
|
assert len(result["organizations"]) == 7
|
||||||
|
|
||||||
|
|
||||||
def test_paginate_orgs(app):
|
def test_paginate_orgs(app):
|
||||||
with client_with_identity("devtable", app) as cl:
|
with client_with_identity("devtable", app) as cl:
|
||||||
params = {"limit": 3}
|
params = {"limit": 4}
|
||||||
firstResult = conduct_api_call(cl, SuperUserOrganizationList, "GET", params, None, 200).json
|
firstResult = conduct_api_call(cl, SuperUserOrganizationList, "GET", params, None, 200).json
|
||||||
assert len(firstResult["organizations"]) == 3
|
assert len(firstResult["organizations"]) == 4
|
||||||
assert firstResult["next_page"] is not None
|
assert firstResult["next_page"] is not None
|
||||||
params["next_page"] = firstResult["next_page"]
|
params["next_page"] = firstResult["next_page"]
|
||||||
secondResult = conduct_api_call(
|
secondResult = conduct_api_call(
|
||||||
@ -50,13 +50,13 @@ def test_paginate_orgs(app):
|
|||||||
|
|
||||||
def test_paginate_test_list_all_users(app):
|
def test_paginate_test_list_all_users(app):
|
||||||
with client_with_identity("devtable", app) as cl:
|
with client_with_identity("devtable", app) as cl:
|
||||||
params = {"limit": 6}
|
params = {"limit": 7}
|
||||||
firstResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json
|
firstResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json
|
||||||
assert len(firstResult["users"]) == 6
|
assert len(firstResult["users"]) == 7
|
||||||
assert firstResult["next_page"] is not None
|
assert firstResult["next_page"] is not None
|
||||||
params["next_page"] = firstResult["next_page"]
|
params["next_page"] = firstResult["next_page"]
|
||||||
secondResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json
|
secondResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json
|
||||||
assert len(secondResult["users"]) == 5
|
assert len(secondResult["users"]) == 4
|
||||||
assert secondResult.get("next_page", None) is None
|
assert secondResult.get("next_page", None) is None
|
||||||
|
|
||||||
|
|
||||||
|
28
initdb.py
28
initdb.py
@ -70,7 +70,10 @@ from data.decorators import is_deprecated_model
|
|||||||
from data.encryption import FieldEncrypter
|
from data.encryption import FieldEncrypter
|
||||||
from data.fields import Credential
|
from data.fields import Credential
|
||||||
from data.logs_model import logs_model
|
from data.logs_model import logs_model
|
||||||
from data.model.autoprune import create_namespace_autoprune_policy
|
from data.model.autoprune import (
|
||||||
|
create_namespace_autoprune_policy,
|
||||||
|
create_repository_autoprune_policy,
|
||||||
|
)
|
||||||
from data.queue import WorkQueue
|
from data.queue import WorkQueue
|
||||||
from data.registry_model import registry_model
|
from data.registry_model import registry_model
|
||||||
from data.registry_model.datatypes import RepositoryReference
|
from data.registry_model.datatypes import RepositoryReference
|
||||||
@ -464,6 +467,10 @@ def initialize_database():
|
|||||||
LogEntryKind.create(name="update_namespace_autoprune_policy")
|
LogEntryKind.create(name="update_namespace_autoprune_policy")
|
||||||
LogEntryKind.create(name="delete_namespace_autoprune_policy")
|
LogEntryKind.create(name="delete_namespace_autoprune_policy")
|
||||||
|
|
||||||
|
LogEntryKind.create(name="create_repository_autoprune_policy")
|
||||||
|
LogEntryKind.create(name="update_repository_autoprune_policy")
|
||||||
|
LogEntryKind.create(name="delete_repository_autoprune_policy")
|
||||||
|
|
||||||
ImageStorageLocation.create(name="local_eu")
|
ImageStorageLocation.create(name="local_eu")
|
||||||
ImageStorageLocation.create(name="local_us")
|
ImageStorageLocation.create(name="local_us")
|
||||||
|
|
||||||
@ -922,6 +929,25 @@ def populate_database(minimal=False):
|
|||||||
"buynlarge", {"method": "creation_date", "value": "5d"}, create_task=True
|
"buynlarge", {"method": "creation_date", "value": "5d"}, create_task=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
org_for_autoprune = model.organization.create_organization(
|
||||||
|
"testorgforautoprune", "autoprune@devtable.com", new_user_1
|
||||||
|
)
|
||||||
|
org_repo = __generate_repository(
|
||||||
|
org_for_autoprune,
|
||||||
|
"autoprunerepo",
|
||||||
|
"Repository owned by an org.",
|
||||||
|
False,
|
||||||
|
[],
|
||||||
|
(4, [], ["latest", "prod"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
create_repository_autoprune_policy(
|
||||||
|
"devtable", simple_repo.name, {"method": "number_of_tags", "value": 10}, create_task=True
|
||||||
|
)
|
||||||
|
create_repository_autoprune_policy(
|
||||||
|
"public", publicrepo.name, {"method": "creation_date", "value": "5d"}, create_task=True
|
||||||
|
)
|
||||||
|
|
||||||
liborg = model.organization.create_organization(
|
liborg = model.organization.create_organization(
|
||||||
"library", "quay+library@devtable.com", new_user_1
|
"library", "quay+library@devtable.com", new_user_1
|
||||||
)
|
)
|
||||||
|
@ -970,11 +970,11 @@ class TestDeleteNamespace(ApiTestCase):
|
|||||||
def test_deletenamespaces(self):
|
def test_deletenamespaces(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
# Try to first delete the user. Since they are the sole admin of three orgs, it should fail.
|
# Try to first delete the user. Since they are the sole admin of five orgs, it should fail.
|
||||||
with check_transitive_modifications():
|
with check_transitive_modifications():
|
||||||
self.deleteResponse(User, expected_code=400)
|
self.deleteResponse(User, expected_code=400)
|
||||||
|
|
||||||
# Delete the three orgs, checking in between.
|
# Delete the five orgs, checking in between.
|
||||||
with check_transitive_modifications():
|
with check_transitive_modifications():
|
||||||
self.deleteEmptyResponse(
|
self.deleteEmptyResponse(
|
||||||
Organization, params=dict(orgname=ORGANIZATION), expected_code=204
|
Organization, params=dict(orgname=ORGANIZATION), expected_code=204
|
||||||
@ -985,9 +985,14 @@ class TestDeleteNamespace(ApiTestCase):
|
|||||||
)
|
)
|
||||||
self.deleteResponse(User, expected_code=400) # Should still fail.
|
self.deleteResponse(User, expected_code=400) # Should still fail.
|
||||||
self.deleteEmptyResponse(Organization, params=dict(orgname="titi"), expected_code=204)
|
self.deleteEmptyResponse(Organization, params=dict(orgname="titi"), expected_code=204)
|
||||||
|
self.deleteResponse(User, expected_code=400) # Should still fail.
|
||||||
self.deleteEmptyResponse(
|
self.deleteEmptyResponse(
|
||||||
Organization, params=dict(orgname="proxyorg"), expected_code=204
|
Organization, params=dict(orgname="proxyorg"), expected_code=204
|
||||||
)
|
)
|
||||||
|
self.deleteResponse(User, expected_code=400) # Should still fail.
|
||||||
|
self.deleteEmptyResponse(
|
||||||
|
Organization, params=dict(orgname="testorgforautoprune"), expected_code=204
|
||||||
|
)
|
||||||
|
|
||||||
# Add some queue items for the user.
|
# Add some queue items for the user.
|
||||||
notification_queue.put([ADMIN_ACCESS_USER, "somerepo", "somename"], "{}")
|
notification_queue.put([ADMIN_ACCESS_USER, "somerepo", "somename"], "{}")
|
||||||
|
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_repository_id_repository;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_pull_robot_id_user;
|
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_pull_robot_id_user;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_access_token_id_accesstoken;
|
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS fk_repositorybuild_access_token_id_accesstoken;
|
||||||
|
ALTER TABLE IF EXISTS ONLY public.repositoryautoprunepolicy DROP CONSTRAINT IF EXISTS fk_repositoryautoprunepolicy_repository_id_repository;
|
||||||
|
ALTER TABLE IF EXISTS ONLY public.repositoryautoprunepolicy DROP CONSTRAINT IF EXISTS fk_repositoryautoprunepolicy_namespace_id_user;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repositoryauthorizedemail DROP CONSTRAINT IF EXISTS fk_repositoryauthorizedemail_repository_id_repository;
|
ALTER TABLE IF EXISTS ONLY public.repositoryauthorizedemail DROP CONSTRAINT IF EXISTS fk_repositoryauthorizedemail_repository_id_repository;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repositoryactioncount DROP CONSTRAINT IF EXISTS fk_repositoryactioncount_repository_id_repository;
|
ALTER TABLE IF EXISTS ONLY public.repositoryactioncount DROP CONSTRAINT IF EXISTS fk_repositoryactioncount_repository_id_repository;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repository DROP CONSTRAINT IF EXISTS fk_repository_visibility_id_visibility;
|
ALTER TABLE IF EXISTS ONLY public.repository DROP CONSTRAINT IF EXISTS fk_repository_visibility_id_visibility;
|
||||||
@ -259,6 +261,9 @@ DROP INDEX IF EXISTS public.repositorybuild_queue_id;
|
|||||||
DROP INDEX IF EXISTS public.repositorybuild_pull_robot_id;
|
DROP INDEX IF EXISTS public.repositorybuild_pull_robot_id;
|
||||||
DROP INDEX IF EXISTS public.repositorybuild_logs_archived;
|
DROP INDEX IF EXISTS public.repositorybuild_logs_archived;
|
||||||
DROP INDEX IF EXISTS public.repositorybuild_access_token_id;
|
DROP INDEX IF EXISTS public.repositorybuild_access_token_id;
|
||||||
|
DROP INDEX IF EXISTS public.repositoryautoprunepolicy_uuid;
|
||||||
|
DROP INDEX IF EXISTS public.repositoryautoprunepolicy_repository_id;
|
||||||
|
DROP INDEX IF EXISTS public.repositoryautoprunepolicy_namespace_id;
|
||||||
DROP INDEX IF EXISTS public.repositoryauthorizedemail_repository_id;
|
DROP INDEX IF EXISTS public.repositoryauthorizedemail_repository_id;
|
||||||
DROP INDEX IF EXISTS public.repositoryauthorizedemail_email_repository_id;
|
DROP INDEX IF EXISTS public.repositoryauthorizedemail_email_repository_id;
|
||||||
DROP INDEX IF EXISTS public.repositoryauthorizedemail_code;
|
DROP INDEX IF EXISTS public.repositoryauthorizedemail_code;
|
||||||
@ -512,6 +517,7 @@ ALTER TABLE IF EXISTS ONLY public.repositorynotification DROP CONSTRAINT IF EXIS
|
|||||||
ALTER TABLE IF EXISTS ONLY public.repositorykind DROP CONSTRAINT IF EXISTS pk_repositorykind;
|
ALTER TABLE IF EXISTS ONLY public.repositorykind DROP CONSTRAINT IF EXISTS pk_repositorykind;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repositorybuildtrigger DROP CONSTRAINT IF EXISTS pk_repositorybuildtrigger;
|
ALTER TABLE IF EXISTS ONLY public.repositorybuildtrigger DROP CONSTRAINT IF EXISTS pk_repositorybuildtrigger;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS pk_repositorybuild;
|
ALTER TABLE IF EXISTS ONLY public.repositorybuild DROP CONSTRAINT IF EXISTS pk_repositorybuild;
|
||||||
|
ALTER TABLE IF EXISTS ONLY public.repositoryautoprunepolicy DROP CONSTRAINT IF EXISTS pk_repositoryautoprunepolicyid;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repositoryauthorizedemail DROP CONSTRAINT IF EXISTS pk_repositoryauthorizedemail;
|
ALTER TABLE IF EXISTS ONLY public.repositoryauthorizedemail DROP CONSTRAINT IF EXISTS pk_repositoryauthorizedemail;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repositoryactioncount DROP CONSTRAINT IF EXISTS pk_repositoryactioncount;
|
ALTER TABLE IF EXISTS ONLY public.repositoryactioncount DROP CONSTRAINT IF EXISTS pk_repositoryactioncount;
|
||||||
ALTER TABLE IF EXISTS ONLY public.repository DROP CONSTRAINT IF EXISTS pk_repository;
|
ALTER TABLE IF EXISTS ONLY public.repository DROP CONSTRAINT IF EXISTS pk_repository;
|
||||||
@ -617,6 +623,7 @@ ALTER TABLE IF EXISTS public.repositorynotification ALTER COLUMN id DROP DEFAULT
|
|||||||
ALTER TABLE IF EXISTS public.repositorykind ALTER COLUMN id DROP DEFAULT;
|
ALTER TABLE IF EXISTS public.repositorykind ALTER COLUMN id DROP DEFAULT;
|
||||||
ALTER TABLE IF EXISTS public.repositorybuildtrigger ALTER COLUMN id DROP DEFAULT;
|
ALTER TABLE IF EXISTS public.repositorybuildtrigger ALTER COLUMN id DROP DEFAULT;
|
||||||
ALTER TABLE IF EXISTS public.repositorybuild ALTER COLUMN id DROP DEFAULT;
|
ALTER TABLE IF EXISTS public.repositorybuild ALTER COLUMN id DROP DEFAULT;
|
||||||
|
ALTER TABLE IF EXISTS public.repositoryautoprunepolicy ALTER COLUMN id DROP DEFAULT;
|
||||||
ALTER TABLE IF EXISTS public.repositoryauthorizedemail ALTER COLUMN id DROP DEFAULT;
|
ALTER TABLE IF EXISTS public.repositoryauthorizedemail ALTER COLUMN id DROP DEFAULT;
|
||||||
ALTER TABLE IF EXISTS public.repositoryactioncount ALTER COLUMN id DROP DEFAULT;
|
ALTER TABLE IF EXISTS public.repositoryactioncount ALTER COLUMN id DROP DEFAULT;
|
||||||
ALTER TABLE IF EXISTS public.repository ALTER COLUMN id DROP DEFAULT;
|
ALTER TABLE IF EXISTS public.repository ALTER COLUMN id DROP DEFAULT;
|
||||||
@ -755,6 +762,8 @@ DROP SEQUENCE IF EXISTS public.repositorybuildtrigger_id_seq;
|
|||||||
DROP TABLE IF EXISTS public.repositorybuildtrigger;
|
DROP TABLE IF EXISTS public.repositorybuildtrigger;
|
||||||
DROP SEQUENCE IF EXISTS public.repositorybuild_id_seq;
|
DROP SEQUENCE IF EXISTS public.repositorybuild_id_seq;
|
||||||
DROP TABLE IF EXISTS public.repositorybuild;
|
DROP TABLE IF EXISTS public.repositorybuild;
|
||||||
|
DROP SEQUENCE IF EXISTS public.repositoryautoprunepolicy_id_seq;
|
||||||
|
DROP TABLE IF EXISTS public.repositoryautoprunepolicy;
|
||||||
DROP SEQUENCE IF EXISTS public.repositoryauthorizedemail_id_seq;
|
DROP SEQUENCE IF EXISTS public.repositoryauthorizedemail_id_seq;
|
||||||
DROP TABLE IF EXISTS public.repositoryauthorizedemail;
|
DROP TABLE IF EXISTS public.repositoryauthorizedemail;
|
||||||
DROP SEQUENCE IF EXISTS public.repositoryactioncount_id_seq;
|
DROP SEQUENCE IF EXISTS public.repositoryactioncount_id_seq;
|
||||||
@ -3542,6 +3551,43 @@ ALTER TABLE public.repositoryauthorizedemail_id_seq OWNER TO quay;
|
|||||||
ALTER SEQUENCE public.repositoryauthorizedemail_id_seq OWNED BY public.repositoryauthorizedemail.id;
|
ALTER SEQUENCE public.repositoryauthorizedemail_id_seq OWNED BY public.repositoryauthorizedemail.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy; Type: TABLE; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.repositoryautoprunepolicy (
|
||||||
|
id integer NOT NULL,
|
||||||
|
uuid character varying(36) NOT NULL,
|
||||||
|
repository_id integer NOT NULL,
|
||||||
|
namespace_id integer NOT NULL,
|
||||||
|
policy text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.repositoryautoprunepolicy OWNER TO quay;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy_id_seq; Type: SEQUENCE; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.repositoryautoprunepolicy_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.repositoryautoprunepolicy_id_seq OWNER TO quay;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.repositoryautoprunepolicy_id_seq OWNED BY public.repositoryautoprunepolicy.id;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: repositorybuild; Type: TABLE; Schema: public; Owner: quay
|
-- Name: repositorybuild; Type: TABLE; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
@ -5309,6 +5355,13 @@ ALTER TABLE ONLY public.repositoryactioncount ALTER COLUMN id SET DEFAULT nextva
|
|||||||
ALTER TABLE ONLY public.repositoryauthorizedemail ALTER COLUMN id SET DEFAULT nextval('public.repositoryauthorizedemail_id_seq'::regclass);
|
ALTER TABLE ONLY public.repositoryauthorizedemail ALTER COLUMN id SET DEFAULT nextval('public.repositoryauthorizedemail_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy id; Type: DEFAULT; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.repositoryautoprunepolicy ALTER COLUMN id SET DEFAULT nextval('public.repositoryautoprunepolicy_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: repositorybuild id; Type: DEFAULT; Schema: public; Owner: quay
|
-- Name: repositorybuild id; Type: DEFAULT; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
@ -5570,7 +5623,7 @@ COPY public.accesstokenkind (id, name) FROM stdin;
|
|||||||
--
|
--
|
||||||
|
|
||||||
COPY public.alembic_version (version_num) FROM stdin;
|
COPY public.alembic_version (version_num) FROM stdin;
|
||||||
41d15c93c299
|
b4da5b09c8df
|
||||||
\.
|
\.
|
||||||
|
|
||||||
|
|
||||||
@ -6242,6 +6295,9 @@ COPY public.logentrykind (id, name) FROM stdin;
|
|||||||
105 push_repo_failed
|
105 push_repo_failed
|
||||||
106 pull_repo_failed
|
106 pull_repo_failed
|
||||||
107 delete_tag_failed
|
107 delete_tag_failed
|
||||||
|
108 create_repository_autoprune_policy
|
||||||
|
109 update_repository_autoprune_policy
|
||||||
|
110 delete_repository_autoprune_policy
|
||||||
\.
|
\.
|
||||||
|
|
||||||
|
|
||||||
@ -6518,8 +6574,8 @@ COPY public.quayservice (id, name) FROM stdin;
|
|||||||
--
|
--
|
||||||
|
|
||||||
COPY public.queueitem (id, queue_name, body, available_after, available, processing_expires, retries_remaining, state_id) FROM stdin;
|
COPY public.queueitem (id, queue_name, body, available_after, available, processing_expires, retries_remaining, state_id) FROM stdin;
|
||||||
1 namespacegc/2/ {"marker_id": 1, "original_username": "quay"} 2023-11-07 19:57:36.102126 t 2023-11-07 22:52:36.037751 5 e90498e4-d92b-487a-90a3-1ab34388af7c
|
2 namespacegc/3/ {"marker_id": 2, "original_username": "clair"} 2024-02-20 14:01:29.891875 t 2024-02-20 16:56:29.836048 5 ff6a0b72-d8cc-4efe-ad83-70225ac325e1
|
||||||
2 namespacegc/3/ {"marker_id": 2, "original_username": "clair"} 2023-11-07 19:57:37.766105 t 2023-11-07 22:52:37.740851 5 9ccfcc76-8277-4dc0-8a14-b37440753124
|
1 namespacegc/2/ {"marker_id": 1, "original_username": "quay"} 2024-02-20 14:01:34.91615 t 2024-02-20 16:56:34.901017 5 6e996e00-e92b-45a9-962f-7e7f3cdb9e63
|
||||||
\.
|
\.
|
||||||
|
|
||||||
|
|
||||||
@ -6928,6 +6984,14 @@ COPY public.repositoryauthorizedemail (id, repository_id, email, code, confirmed
|
|||||||
\.
|
\.
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: repositoryautoprunepolicy; Type: TABLE DATA; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
COPY public.repositoryautoprunepolicy (id, uuid, repository_id, namespace_id, policy) FROM stdin;
|
||||||
|
\.
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: repositorybuild; Type: TABLE DATA; Schema: public; Owner: quay
|
-- Data for Name: repositorybuild; Type: TABLE DATA; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
@ -8084,7 +8148,7 @@ SELECT pg_catalog.setval('public.logentry_id_seq', 1, false);
|
|||||||
-- Name: logentrykind_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
-- Name: logentrykind_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
|
|
||||||
SELECT pg_catalog.setval('public.logentrykind_id_seq', 107, true);
|
SELECT pg_catalog.setval('public.logentrykind_id_seq', 110, true);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -8168,7 +8232,7 @@ SELECT pg_catalog.setval('public.namespacegeorestriction_id_seq', 1, false);
|
|||||||
-- Name: notification_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
-- Name: notification_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
|
|
||||||
SELECT pg_catalog.setval('public.notification_id_seq', 1, false);
|
SELECT pg_catalog.setval('public.notification_id_seq', 1, true);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -8325,6 +8389,13 @@ SELECT pg_catalog.setval('public.repositoryactioncount_id_seq', 157, true);
|
|||||||
SELECT pg_catalog.setval('public.repositoryauthorizedemail_id_seq', 1, true);
|
SELECT pg_catalog.setval('public.repositoryauthorizedemail_id_seq', 1, true);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
SELECT pg_catalog.setval('public.repositoryautoprunepolicy_id_seq', 1, false);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: repositorybuild_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
-- Name: repositorybuild_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
@ -8406,14 +8477,14 @@ SELECT pg_catalog.setval('public.role_id_seq', 3, true);
|
|||||||
-- Name: servicekey_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
-- Name: servicekey_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
|
|
||||||
SELECT pg_catalog.setval('public.servicekey_id_seq', 1, false);
|
SELECT pg_catalog.setval('public.servicekey_id_seq', 1, true);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: servicekeyapproval_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
-- Name: servicekeyapproval_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
|
|
||||||
SELECT pg_catalog.setval('public.servicekeyapproval_id_seq', 1, false);
|
SELECT pg_catalog.setval('public.servicekeyapproval_id_seq', 1, true);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -9131,6 +9202,14 @@ ALTER TABLE ONLY public.repositoryauthorizedemail
|
|||||||
ADD CONSTRAINT pk_repositoryauthorizedemail PRIMARY KEY (id);
|
ADD CONSTRAINT pk_repositoryauthorizedemail PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy pk_repositoryautoprunepolicyid; Type: CONSTRAINT; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.repositoryautoprunepolicy
|
||||||
|
ADD CONSTRAINT pk_repositoryautoprunepolicyid PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: repositorybuild pk_repositorybuild; Type: CONSTRAINT; Schema: public; Owner: quay
|
-- Name: repositorybuild pk_repositorybuild; Type: CONSTRAINT; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
@ -10936,6 +11015,27 @@ CREATE UNIQUE INDEX repositoryauthorizedemail_email_repository_id ON public.repo
|
|||||||
CREATE INDEX repositoryauthorizedemail_repository_id ON public.repositoryauthorizedemail USING btree (repository_id);
|
CREATE INDEX repositoryauthorizedemail_repository_id ON public.repositoryauthorizedemail USING btree (repository_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy_namespace_id; Type: INDEX; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX repositoryautoprunepolicy_namespace_id ON public.repositoryautoprunepolicy USING btree (namespace_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy_repository_id; Type: INDEX; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX repositoryautoprunepolicy_repository_id ON public.repositoryautoprunepolicy USING btree (repository_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy_uuid; Type: INDEX; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX repositoryautoprunepolicy_uuid ON public.repositoryautoprunepolicy USING btree (uuid);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: repositorybuild_access_token_id; Type: INDEX; Schema: public; Owner: quay
|
-- Name: repositorybuild_access_token_id; Type: INDEX; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
@ -12411,6 +12511,22 @@ ALTER TABLE ONLY public.repositoryauthorizedemail
|
|||||||
ADD CONSTRAINT fk_repositoryauthorizedemail_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id);
|
ADD CONSTRAINT fk_repositoryauthorizedemail_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy fk_repositoryautoprunepolicy_namespace_id_user; Type: FK CONSTRAINT; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.repositoryautoprunepolicy
|
||||||
|
ADD CONSTRAINT fk_repositoryautoprunepolicy_namespace_id_user FOREIGN KEY (namespace_id) REFERENCES public."user"(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: repositoryautoprunepolicy fk_repositoryautoprunepolicy_repository_id_repository; Type: FK CONSTRAINT; Schema: public; Owner: quay
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.repositoryautoprunepolicy
|
||||||
|
ADD CONSTRAINT fk_repositoryautoprunepolicy_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: repositorybuild fk_repositorybuild_access_token_id_accesstoken; Type: FK CONSTRAINT; Schema: public; Owner: quay
|
-- Name: repositorybuild fk_repositorybuild_access_token_id_accesstoken; Type: FK CONSTRAINT; Schema: public; Owner: quay
|
||||||
--
|
--
|
||||||
|
@ -10,15 +10,20 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
|||||||
export function useNamespaceAutoPrunePolicies(
|
export function useNamespaceAutoPrunePolicies(
|
||||||
namespace: string,
|
namespace: string,
|
||||||
isUser: boolean,
|
isUser: boolean,
|
||||||
|
isEnabled: boolean = true,
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
data: policies,
|
data: nsPolicies,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
dataUpdatedAt,
|
dataUpdatedAt,
|
||||||
} = useQuery(['namespace', 'autoprunepolicies', namespace], ({signal}) =>
|
} = useQuery(
|
||||||
fetchNamespaceAutoPrunePolicies(namespace, isUser, signal),
|
['namespace', 'autoprunepolicies', namespace],
|
||||||
|
({signal}) => fetchNamespaceAutoPrunePolicies(namespace, isUser, signal),
|
||||||
|
{
|
||||||
|
enabled: isEnabled,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -26,7 +31,7 @@ export function useNamespaceAutoPrunePolicies(
|
|||||||
isSuccess,
|
isSuccess,
|
||||||
isLoading,
|
isLoading,
|
||||||
dataUpdatedAt,
|
dataUpdatedAt,
|
||||||
policies,
|
nsPolicies,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 TeamsAndMembershipList from './Tabs/TeamsAndMembership/TeamsAndMembershipList';
|
||||||
import AddNewTeamMemberDrawer from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/AddNewTeamMemberDrawer';
|
import AddNewTeamMemberDrawer from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/AddNewTeamMemberDrawer';
|
||||||
import ManageMembersList from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList';
|
import ManageMembersList from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList';
|
||||||
import UsageLogs from 'src/routes/UsageLogs/UsageLogs';
|
|
||||||
|
|
||||||
export enum OrganizationDrawerContentType {
|
export enum OrganizationDrawerContentType {
|
||||||
None,
|
None,
|
||||||
|
@ -50,7 +50,7 @@ export default function AutoPruning(props: AutoPruning) {
|
|||||||
error,
|
error,
|
||||||
isSuccess: successFetchingPolicies,
|
isSuccess: successFetchingPolicies,
|
||||||
isLoading,
|
isLoading,
|
||||||
policies,
|
nsPolicies,
|
||||||
dataUpdatedAt,
|
dataUpdatedAt,
|
||||||
} = useNamespaceAutoPrunePolicies(props.org, props.isUser);
|
} = useNamespaceAutoPrunePolicies(props.org, props.isUser);
|
||||||
const {
|
const {
|
||||||
@ -76,8 +76,8 @@ export default function AutoPruning(props: AutoPruning) {
|
|||||||
if (successFetchingPolicies) {
|
if (successFetchingPolicies) {
|
||||||
// Currently we only support one policy per namespace but
|
// Currently we only support one policy per namespace but
|
||||||
// this will change in the future.
|
// this will change in the future.
|
||||||
if (policies.length > 0) {
|
if (nsPolicies.length > 0) {
|
||||||
const policy: NamespaceAutoPrunePolicy = policies[0];
|
const policy: NamespaceAutoPrunePolicy = nsPolicies[0];
|
||||||
setMethod(policy.method);
|
setMethod(policy.method);
|
||||||
setUuid(policy.uuid);
|
setUuid(policy.uuid);
|
||||||
switch (policy.method) {
|
switch (policy.method) {
|
||||||
|
@ -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 {RepositoryStateForm} from './RepositoryState';
|
||||||
import {RepositoryDetails} from 'src/resources/RepositoryResource';
|
import {RepositoryDetails} from 'src/resources/RepositoryResource';
|
||||||
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
|
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
|
||||||
|
import RepositoryAutoPruning from 'src/routes/RepositoryDetails/Settings/RepositoryAutoPruning';
|
||||||
|
import {useOrganization} from 'src/hooks/UseOrganization';
|
||||||
|
|
||||||
export default function Settings(props: SettingsProps) {
|
export default function Settings(props: SettingsProps) {
|
||||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||||
const config = useQuayConfig();
|
const config = useQuayConfig();
|
||||||
|
const {isUserOrganization} = useOrganization(props.org);
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: 'User and robot permissions',
|
name: 'User and robot permissions',
|
||||||
@ -24,6 +28,21 @@ export default function Settings(props: SettingsProps) {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
...(config?.features?.AUTO_PRUNE
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'Repository Auto-Prune Policies',
|
||||||
|
id: 'repositoryautoprunepolicies',
|
||||||
|
content: (
|
||||||
|
<RepositoryAutoPruning
|
||||||
|
organizationName={props.org}
|
||||||
|
repoName={props.repo}
|
||||||
|
isUser={isUserOrganization}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
name: 'Events and notifications',
|
name: 'Events and notifications',
|
||||||
id: 'eventsandnotifications',
|
id: 'eventsandnotifications',
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import logging.config
|
import logging.config
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import features
|
import features
|
||||||
@ -35,21 +37,42 @@ class AutoPruneWorker(Worker):
|
|||||||
autoprune_task.id,
|
autoprune_task.id,
|
||||||
autoprune_task.namespace,
|
autoprune_task.namespace,
|
||||||
)
|
)
|
||||||
|
repo_policies = []
|
||||||
try:
|
try:
|
||||||
policies = get_namespace_autoprune_policies_by_id(autoprune_task.namespace)
|
ns_policies = get_namespace_autoprune_policies_by_id(autoprune_task.namespace)
|
||||||
if not policies:
|
if not ns_policies:
|
||||||
# When implementing repo policies, fetch repo policies before deleting the task
|
repo_policies = get_repository_autoprune_policies_by_namespace_id(
|
||||||
delete_autoprune_task(autoprune_task)
|
autoprune_task.namespace
|
||||||
continue
|
)
|
||||||
|
if not repo_policies:
|
||||||
|
logger.info(
|
||||||
|
"deleting autoprune task %s for namespace %s",
|
||||||
|
autoprune_task.id,
|
||||||
|
autoprune_task.namespace,
|
||||||
|
)
|
||||||
|
delete_autoprune_task(autoprune_task)
|
||||||
|
continue
|
||||||
|
|
||||||
execute_namespace_polices(
|
execute_namespace_polices(
|
||||||
policies,
|
ns_policies,
|
||||||
autoprune_task.namespace,
|
autoprune_task.namespace,
|
||||||
FETCH_REPOSITORIES_PAGE_LIMIT,
|
FETCH_REPOSITORIES_PAGE_LIMIT,
|
||||||
FETCH_TAGS_PAGE_LIMIT,
|
FETCH_TAGS_PAGE_LIMIT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# case: only repo policies exists & no namespace policy
|
||||||
|
for policy in repo_policies:
|
||||||
|
repo_id = policy.repository_id
|
||||||
|
repo = get_repository_by_policy_repo_id(repo_id)
|
||||||
|
logger.info(
|
||||||
|
"processing autoprune task %s for repository %s",
|
||||||
|
autoprune_task.id,
|
||||||
|
repo.name,
|
||||||
|
)
|
||||||
|
execute_policy_on_repo(
|
||||||
|
policy, repo_id, autoprune_task.namespace, tag_page_limit=100
|
||||||
|
)
|
||||||
|
|
||||||
update_autoprune_task(autoprune_task, task_status="success")
|
update_autoprune_task(autoprune_task, task_status="success")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
update_autoprune_task(autoprune_task, task_status=f"failure: {str(err)}")
|
update_autoprune_task(autoprune_task, task_status=f"failure: {str(err)}")
|
||||||
|
Reference in New Issue
Block a user