1
0
mirror of https://github.com/quay/quay.git synced 2026-01-26 06:21:37 +03:00
Files
quay/data/model/namespacequota.py
Brandon Caton c843bf2104 quota: only create notification if one doesn't already exist (PROJQUAY-8857) (#4092)
Add check if a notification exists before creating another. Previously many notifications were being created causing tables to inflate.
2025-09-02 09:12:32 -04:00

320 lines
10 KiB
Python

import json
from peewee import JOIN, DataError, fn
from data import model
from data.database import (
ImageStorage,
Manifest,
ManifestBlob,
QuotaLimits,
QuotaNamespaceSize,
QuotaType,
QuotaTypes,
Repository,
Tag,
User,
UserOrganizationQuota,
get_epoch_timestamp_ms,
)
from data.model import (
InvalidNamespaceQuota,
InvalidNamespaceQuotaLimit,
InvalidNamespaceQuotaType,
InvalidOrganizationException,
InvalidSystemQuotaConfig,
InvalidUsernameException,
UnsupportedQuotaSize,
config,
db_transaction,
notification,
organization,
quota,
repository,
user,
)
def get_namespace_quota_list(namespace_name):
quotas = UserOrganizationQuota.select().join(User).where(User.username == namespace_name)
return list(quotas)
def get_namespace_quota(namespace_name, quota_id):
quota = (
UserOrganizationQuota.select()
.join(User)
.where(
User.username == namespace_name,
UserOrganizationQuota.id == quota_id,
)
)
quota = quota.first()
return quota
def create_namespace_quota(namespace_user, limit_bytes):
try:
UserOrganizationQuota.get(id == namespace_user.id)
raise InvalidNamespaceQuota("Only one quota per namespace is currently supported")
except UserOrganizationQuota.DoesNotExist:
pass
if limit_bytes > 0:
try:
return UserOrganizationQuota.create(namespace=namespace_user, limit_bytes=limit_bytes)
except DataError:
raise UnsupportedQuotaSize("Unsupported quota size limit.")
except model.DataModelException as ex:
return None
else:
raise InvalidNamespaceQuota("Invalid quota size limit value: '%s'" % limit_bytes)
def get_system_default_quota(namespace=None):
return UserOrganizationQuota(
namespace=namespace, limit_bytes=config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES")
)
def update_namespace_quota_size(quota, limit_bytes):
if limit_bytes > 0:
try:
quota.limit_bytes = limit_bytes
quota.save()
except DataError:
raise UnsupportedQuotaSize("Unsupported quota size limit.")
else:
raise InvalidNamespaceQuota("Invalid quota size limit value: '%s'" % limit_bytes)
def delete_namespace_quota(quota):
with db_transaction():
QuotaLimits.delete().where(QuotaLimits.quota == quota).execute()
quota.delete_instance()
def _quota_type(type_name):
if type_name.lower() == "warning":
return QuotaTypes.WARNING
elif type_name.lower() == "reject":
return QuotaTypes.REJECT
raise InvalidNamespaceQuotaType(
"Quota type must be one of [{}, {}]".format(QuotaTypes.WARNING, QuotaTypes.REJECT)
)
def get_namespace_quota_limit_list(quota, quota_type=None, percent_of_limit=None):
if not quota:
return []
if percent_of_limit and (not percent_of_limit > 0 or not percent_of_limit <= 100):
raise InvalidNamespaceQuotaLimit("Quota limit threshold must be between 1 and 100")
query = QuotaLimits.select().join(QuotaType).where(QuotaLimits.quota == quota)
if quota_type:
quota_type_name = _quota_type(quota_type)
query = query.where(QuotaType.name == quota_type_name)
if percent_of_limit:
query = query.where(QuotaLimits.percent_of_limit == percent_of_limit)
return list(query)
def get_namespace_quota_limit(quota, limit_id):
try:
quota_limit = QuotaLimits.get(QuotaLimits.id == limit_id)
# This should never happen in theory, as limit ids should be globally unique
if quota_limit.quota != quota:
raise InvalidNamespaceQuota()
return quota_limit
except QuotaLimits.DoesNotExist:
return None
def create_namespace_quota_limit(quota, quota_type, percent_of_limit):
if not percent_of_limit > 0 or not percent_of_limit <= 100:
raise InvalidNamespaceQuotaLimit("Quota limit threshold must be between 1 and 100")
quota_type_name = _quota_type(quota_type)
return QuotaLimits.create(
quota=quota,
percent_of_limit=percent_of_limit,
quota_type=(QuotaType.get(QuotaType.name == quota_type_name)),
)
def update_namespace_quota_limit_threshold(limit, percent_of_limit):
if not percent_of_limit > 0 or not percent_of_limit <= 100:
raise InvalidNamespaceQuotaLimit("Quota limit threshold must be between 1 and 100")
limit.percent_of_limit = percent_of_limit
limit.save()
def update_namespace_quota_limit_type(limit, type_name):
quota_type_name = _quota_type(type_name)
quota_type_ref = QuotaType.get(name=quota_type_name)
limit.quota_type = quota_type_ref
limit.save()
def delete_namespace_quota_limit(limit):
limit.delete_instance()
def verify_namespace_quota(repository_ref):
namespace_size = get_namespace_size(repository_ref.namespace_name)
return check_limits(repository_ref.namespace_name, namespace_size)
def verify_namespace_quota_during_upload(repository_ref):
size = model.repository.get_size_during_upload(repository_ref._db_id)
namespace_size = get_namespace_size(repository_ref.namespace_name)
return check_limits(repository_ref.namespace_name, size + namespace_size)
def check_limits(namespace_name, size):
namespace_user = model.user.get_user_or_org(namespace_name)
quotas = get_namespace_quota_list(namespace_user.username)
if not quotas:
default_size = config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES", 0)
return {
"limit_bytes": default_size,
"severity_level": None
if (default_size == 0 or size < default_size)
else QuotaTypes.REJECT,
}
# Currently only one quota per namespace is supported
quota = quotas[0]
limits = get_namespace_quota_limit_list(quota)
limit_bytes = 0
severity_level = None
for limit in limits:
bytes_allowed = int(limit.quota.limit_bytes * limit.percent_of_limit / 100)
if size > bytes_allowed:
if limit_bytes < bytes_allowed:
limit_bytes = bytes_allowed
severity_level = limit.quota_type.name
return {"limit_bytes": limit_bytes, "severity_level": severity_level}
def notify_organization_admins(repository_ref, notification_kind, metadata={}):
namespace_user = model.user.get_namespace_user(repository_ref.namespace_name)
if namespace_user is None:
raise InvalidUsernameException(
"Namespace '%s' does not exist" % repository_ref.namespace_name
)
metadata.update({"namespace": repository_ref.namespace_name})
if namespace_user.organization:
admins = organization.get_admin_users(namespace_user)
for admin in admins:
# Check if notification already exists with the same kind and metadata
if not notification.notification_exists_with_metadata(
admin, notification_kind, **metadata
):
notification.create_notification(
notification_kind,
admin,
metadata,
)
else:
# Check if notification already exists with the same kind and metadata
if not notification.notification_exists_with_metadata(
namespace_user, notification_kind, **metadata
):
notification.create_notification(
notification_kind,
namespace_user,
metadata,
)
def get_namespace_size(namespace_name):
namespace = user.get_user_or_org(namespace_name)
try:
namespace_size_row = (
QuotaNamespaceSize.select().where(QuotaNamespaceSize.namespace_user == namespace).get()
)
return namespace_size_row.size_bytes if namespace_size_row.size_bytes is not None else 0
except QuotaNamespaceSize.DoesNotExist:
return 0
def fetch_system_default(quotas):
if not quotas and config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES") != 0:
return config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES")
return None
def get_repo_quota_for_view(namespace_name, repo_name):
repository_ref = model.repository.get_repository(namespace_name, repo_name)
if not repository_ref:
return None
quotas = get_namespace_quota_list(repository_ref.namespace_user.username)
# Currently only one quota per namespace is supported
configured_namespace_quota = quotas[0].limit_bytes if quotas else fetch_system_default(quotas)
repo_size = model.repository.get_repository_size(repository_ref.id)
# If FEATURE_QUOTA_MANAGEMENT is enabled & quota is not set for an org,
# we still want to report repo's storage consumption
return {
"quota_bytes": repo_size,
"configured_quota": configured_namespace_quota,
}
def get_quota_for_view(namespace_name):
namespace_user = model.user.get_user_or_org(namespace_name)
quotas = get_namespace_quota_list(namespace_user.username)
# Currently only one quota per namespace is supported
configured_namespace_quota = quotas[0].limit_bytes if quotas else fetch_system_default(quotas)
namespace_size = quota.get_namespace_size(namespace_user.id)
namespace_size_exists = namespace_size is not None
backfill_started = namespace_size_exists and namespace_size.backfill_start_ms is not None
namespace_quota_consumed = (
namespace_size.size_bytes
if namespace_size_exists and namespace_size.size_bytes is not None
else 0
)
backfill_status = ""
if not namespace_size_exists or (not backfill_started and not namespace_size.backfill_complete):
backfill_status = "waiting"
elif backfill_started and not namespace_size.backfill_complete:
backfill_status = "running"
elif backfill_started and namespace_size.backfill_complete:
backfill_status = "complete"
# If FEATURE_QUOTA_MANAGEMENT is enabled & quota is not set for an org,
# we still want to report org's storage consumption.
# TODO: Remove running_backfill when changing API fields is permitted,
# backfill_status should be used in favor of running_backfill
return {
"quota_bytes": namespace_quota_consumed,
"configured_quota": configured_namespace_quota,
"running_backfill": backfill_status,
"backfill_status": backfill_status,
}