mirror of
https://github.com/quay/quay.git
synced 2025-07-30 07:43:13 +03:00
api: update the quota api so that it's more consistent with the other apis endpoints (PROJQUAY-2936) (#1221)
* api: update the quota api so that it's more consistent with the other apis (PROJQUAY-2936) - Uodate the quota api to be more consistent with the rest of the endpoints - Handles some uncaught exceptions, such as division by zero - Update some of the quota data models used by the api to take object references instead of names to make it easier to use - Update table model naming conventions - swagger operationid multiple nicknames - Added more test cases for api - Remove unused functions - Update the UI for better UX, based on the api changes made * quota: fix ui input form value * quota: join quota type query * Remove unused functions
This commit is contained in:
committed by
GitHub
parent
a79f7b6f40
commit
896a3aab3a
@ -769,18 +769,23 @@ class RobotAccountToken(BaseModel):
|
|||||||
fully_migrated = BooleanField(default=False)
|
fully_migrated = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaTypes(object):
|
||||||
|
WARNING = "Warning"
|
||||||
|
REJECT = "Reject"
|
||||||
|
|
||||||
|
|
||||||
class QuotaType(BaseModel):
|
class QuotaType(BaseModel):
|
||||||
name = CharField()
|
name = CharField()
|
||||||
|
|
||||||
|
|
||||||
class UserOrganizationQuota(BaseModel):
|
class UserOrganizationQuota(BaseModel):
|
||||||
namespace_id = QuayUserField(index=True, unique=True)
|
namespace = QuayUserField(index=True, unique=True)
|
||||||
limit_bytes = BigIntegerField()
|
limit_bytes = BigIntegerField()
|
||||||
|
|
||||||
|
|
||||||
class QuotaLimits(BaseModel):
|
class QuotaLimits(BaseModel):
|
||||||
quota_id = ForeignKeyField(UserOrganizationQuota)
|
quota = ForeignKeyField(UserOrganizationQuota)
|
||||||
quota_type_id = ForeignKeyField(QuotaType)
|
quota_type = ForeignKeyField(QuotaType)
|
||||||
percent_of_limit = IntegerField(default=0)
|
percent_of_limit = IntegerField(default=0)
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,6 +121,18 @@ class QuotaExceededException(DataModelException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidNamespaceQuota(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidNamespaceQuotaLimit(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidNamespaceQuotaType(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)
|
||||||
|
@ -12,18 +12,145 @@ from data.database import (
|
|||||||
Tag,
|
Tag,
|
||||||
RepositorySize,
|
RepositorySize,
|
||||||
User,
|
User,
|
||||||
|
QuotaTypes,
|
||||||
)
|
)
|
||||||
from data import model
|
from data import model
|
||||||
from data.model import (
|
from data.model import (
|
||||||
|
db_transaction,
|
||||||
organization,
|
organization,
|
||||||
user,
|
user,
|
||||||
InvalidUsernameException,
|
InvalidUsernameException,
|
||||||
|
InvalidOrganizationException,
|
||||||
notification,
|
notification,
|
||||||
config,
|
config,
|
||||||
InvalidSystemQuotaConfig,
|
InvalidSystemQuotaConfig,
|
||||||
|
InvalidNamespaceQuota,
|
||||||
|
InvalidNamespaceQuotaLimit,
|
||||||
|
InvalidNamespaceQuotaType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 model.DataModelException as ex:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise InvalidNamespaceQuota("Invalid quota size limit value: '%s'" % limit_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def update_namespace_quota_size(quota, limit_bytes):
|
||||||
|
if limit_bytes > 0:
|
||||||
|
quota.limit_bytes = limit_bytes
|
||||||
|
quota.save()
|
||||||
|
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 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):
|
def verify_namespace_quota(repository_ref):
|
||||||
model.repository.get_repository_size_and_cache(repository_ref._db_id)
|
model.repository.get_repository_size_and_cache(repository_ref._db_id)
|
||||||
namespace_size = get_namespace_size(repository_ref.namespace_name)
|
namespace_size = get_namespace_size(repository_ref.namespace_name)
|
||||||
@ -46,14 +173,23 @@ def verify_namespace_quota_during_upload(repository_ref):
|
|||||||
|
|
||||||
|
|
||||||
def check_limits(namespace_name, size):
|
def check_limits(namespace_name, size):
|
||||||
limits = get_namespace_limits(namespace_name)
|
namespace_user = model.user.get_user_or_org(namespace_name)
|
||||||
|
quotas = get_namespace_quota_list(namespace_user.username)
|
||||||
|
if not quotas:
|
||||||
|
return {"limit_bytes": 0, "severity_level": None}
|
||||||
|
|
||||||
|
# Currently only one quota per namespace is supported
|
||||||
|
quota = quotas[0]
|
||||||
|
limits = get_namespace_quota_limit_list(quota)
|
||||||
limit_bytes = 0
|
limit_bytes = 0
|
||||||
severity_level = None
|
severity_level = None
|
||||||
|
|
||||||
for limit in limits:
|
for limit in limits:
|
||||||
if size > limit["bytes_allowed"]:
|
bytes_allowed = int(limit.quota.limit_bytes * limit.percent_of_limit / 100)
|
||||||
if limit_bytes < limit["bytes_allowed"]:
|
if size > bytes_allowed:
|
||||||
limit_bytes = limit["bytes_allowed"]
|
if limit_bytes < bytes_allowed:
|
||||||
severity_level = limit["name"]
|
limit_bytes = bytes_allowed
|
||||||
|
severity_level = limit.quota_type.name
|
||||||
|
|
||||||
return {"limit_bytes": limit_bytes, "severity_level": severity_level}
|
return {"limit_bytes": limit_bytes, "severity_level": severity_level}
|
||||||
|
|
||||||
@ -82,241 +218,6 @@ def force_cache_repo_size(repository_ref):
|
|||||||
return model.repository.force_cache_repo_size(repository_ref._db_id)
|
return model.repository.force_cache_repo_size(repository_ref._db_id)
|
||||||
|
|
||||||
|
|
||||||
def create_namespace_quota(name, limit_bytes):
|
|
||||||
user_object = user.get_namespace_user(name)
|
|
||||||
try:
|
|
||||||
return UserOrganizationQuota.create(namespace_id=user_object.id, limit_bytes=limit_bytes)
|
|
||||||
except model.DataModelException as ex:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def create_namespace_limit(orgname, quota_type_id, percent_of_limit):
|
|
||||||
quota = get_namespace_quota(orgname)
|
|
||||||
|
|
||||||
if quota is None:
|
|
||||||
raise InvalidUsernameException("Quota Does Not Exist for : " + orgname)
|
|
||||||
|
|
||||||
new_limit = QuotaLimits.create(
|
|
||||||
quota_id=quota.id,
|
|
||||||
percent_of_limit=percent_of_limit,
|
|
||||||
quota_type_id=quota_type_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return new_limit
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_quota(name):
|
|
||||||
try:
|
|
||||||
space = user.get_namespace_user(name)
|
|
||||||
if space is None:
|
|
||||||
raise InvalidUsernameException("This Namespace does not exist : " + name)
|
|
||||||
|
|
||||||
quota = UserOrganizationQuota.select().where(UserOrganizationQuota.namespace_id == space.id)
|
|
||||||
# TODO: I dont like this so we will need to find a better way to test if the query is empty.
|
|
||||||
return quota.get()
|
|
||||||
except UserOrganizationQuota.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def check_system_quota_bytes_enabled():
|
|
||||||
|
|
||||||
if config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES") < 0:
|
|
||||||
raise InvalidSystemQuotaConfig(
|
|
||||||
"Invalid Configuration: Quota bytes must be greater than or equal to 0"
|
|
||||||
)
|
|
||||||
|
|
||||||
if config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES") != 0:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_limits(name):
|
|
||||||
query = (
|
|
||||||
UserOrganizationQuota.select(
|
|
||||||
UserOrganizationQuota.limit_bytes,
|
|
||||||
QuotaLimits.percent_of_limit,
|
|
||||||
QuotaLimits.quota_id,
|
|
||||||
QuotaLimits.id,
|
|
||||||
QuotaType.name,
|
|
||||||
QuotaType.id,
|
|
||||||
(
|
|
||||||
UserOrganizationQuota.limit_bytes.cast("decimal")
|
|
||||||
* (QuotaLimits.percent_of_limit.cast("decimal") / 100.0).cast("decimal")
|
|
||||||
).alias("bytes_allowed"),
|
|
||||||
QuotaType.id.alias("type_id"),
|
|
||||||
)
|
|
||||||
.join(User, on=(UserOrganizationQuota.namespace_id == User.id))
|
|
||||||
.join(QuotaLimits, on=(UserOrganizationQuota.id == QuotaLimits.quota_id))
|
|
||||||
.join(QuotaType, on=(QuotaLimits.quota_type_id == QuotaType.id))
|
|
||||||
.where(User.username == name)
|
|
||||||
).dicts()
|
|
||||||
|
|
||||||
# define limits if a system default is defined in config.py and no namespace specific limits are set
|
|
||||||
if check_system_quota_bytes_enabled() and len(query) == 0:
|
|
||||||
query = [
|
|
||||||
{
|
|
||||||
"limit_bytes": config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES"),
|
|
||||||
"percent_of_limit": 80,
|
|
||||||
"name": "System Warning Limit",
|
|
||||||
"bytes_allowed": config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES") * 0.8,
|
|
||||||
"type_id": 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"limit_bytes": config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES"),
|
|
||||||
"percent_of_limit": 100,
|
|
||||||
"name": "System Reject Limit",
|
|
||||||
"bytes_allowed": config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES"),
|
|
||||||
"type_id": 2,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_limit(name, quota_type_id, percent_of_limit):
|
|
||||||
try:
|
|
||||||
quota = get_namespace_quota(name)
|
|
||||||
|
|
||||||
if quota is None:
|
|
||||||
raise InvalidUsernameException("Quota for this namespace does not exist")
|
|
||||||
|
|
||||||
query = (
|
|
||||||
QuotaLimits.select()
|
|
||||||
.join(QuotaType)
|
|
||||||
.where(QuotaLimits.quota_id == quota.id)
|
|
||||||
.where(QuotaLimits.quota_type_id == quota_type_id)
|
|
||||||
.where(QuotaLimits.percent_of_limit == percent_of_limit)
|
|
||||||
)
|
|
||||||
|
|
||||||
return query.get()
|
|
||||||
|
|
||||||
except QuotaLimits.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_limit_from_id(name, quota_limit_id):
|
|
||||||
try:
|
|
||||||
quota = get_namespace_quota(name)
|
|
||||||
|
|
||||||
if quota is None:
|
|
||||||
raise InvalidUsernameException("Quota for this namespace does not exist")
|
|
||||||
|
|
||||||
query = (
|
|
||||||
QuotaLimits.select()
|
|
||||||
.join(QuotaType)
|
|
||||||
.where(QuotaLimits.quota_id == quota.id)
|
|
||||||
.where(QuotaLimits.id == quota_limit_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return query.get()
|
|
||||||
|
|
||||||
except QuotaLimits.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_reject_limit(name):
|
|
||||||
try:
|
|
||||||
quota = get_namespace_quota(name)
|
|
||||||
|
|
||||||
if quota is None:
|
|
||||||
raise InvalidUsernameException("Quota for this namespace does not exist")
|
|
||||||
|
|
||||||
# QuotaType
|
|
||||||
query = (
|
|
||||||
QuotaLimits.select()
|
|
||||||
.join(QuotaType)
|
|
||||||
.where(QuotaType.name == "Reject")
|
|
||||||
.where(QuotaLimits.quota_id == quota.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return query.get()
|
|
||||||
|
|
||||||
except QuotaLimits.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_limit_types():
|
|
||||||
return [{"quota_type_id": qtype.id, "name": qtype.name} for qtype in QuotaType.select()]
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_limit_id_from_name(name):
|
|
||||||
for i in get_namespace_limit_types():
|
|
||||||
if name == i["name"]:
|
|
||||||
return i["quota_type_id"]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def is_reject_limit_type(quota_type_id):
|
|
||||||
if quota_type_id == fetch_limit_id_from_name("Reject"):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_limit_types_for_id(quota_limit_type_id):
|
|
||||||
return QuotaType.select().where(QuotaType.id == quota_limit_type_id).get()
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_limit_types_for_name(name):
|
|
||||||
return QuotaType.select().where(QuotaType.name == name).get()
|
|
||||||
|
|
||||||
|
|
||||||
def change_namespace_quota(name, limit_bytes):
|
|
||||||
org = user.get_namespace_user(name)
|
|
||||||
quota = UserOrganizationQuota.select().where(UserOrganizationQuota.namespace_id == org.id).get()
|
|
||||||
|
|
||||||
quota.limit_bytes = limit_bytes
|
|
||||||
quota.save()
|
|
||||||
|
|
||||||
return quota
|
|
||||||
|
|
||||||
|
|
||||||
def change_namespace_quota_limit(name, percent_of_limit, quota_type_id, quota_limit_id):
|
|
||||||
quota_limit = get_namespace_limit_from_id(name, quota_limit_id)
|
|
||||||
|
|
||||||
quota_limit.percent_of_limit = percent_of_limit
|
|
||||||
quota_limit.quota_type_id = quota_type_id
|
|
||||||
quota_limit.save()
|
|
||||||
|
|
||||||
return quota_limit
|
|
||||||
|
|
||||||
|
|
||||||
def delete_namespace_quota_limit(name, quota_limit_id):
|
|
||||||
quota_limit = get_namespace_limit_from_id(name, quota_limit_id)
|
|
||||||
|
|
||||||
if quota_limit is not None:
|
|
||||||
quota_limit.delete_instance()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def delete_all_namespace_quota_limits(quota):
|
|
||||||
return QuotaLimits.delete().where(QuotaLimits.quota_id == quota.id).execute()
|
|
||||||
|
|
||||||
|
|
||||||
def delete_namespace_quota(name):
|
|
||||||
org = user.get_namespace_user(name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
quota = (
|
|
||||||
UserOrganizationQuota.select().where(UserOrganizationQuota.namespace_id == org.id)
|
|
||||||
).get()
|
|
||||||
except UserOrganizationQuota.DoesNotExist:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if quota is not None:
|
|
||||||
delete_all_namespace_quota_limits(quota)
|
|
||||||
UserOrganizationQuota.delete().where(UserOrganizationQuota.namespace_id == org.id).execute()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_repository_sizes_and_cache(namespace_name):
|
|
||||||
return cache_namespace_repository_sizes(namespace_name)
|
|
||||||
|
|
||||||
|
|
||||||
def cache_namespace_repository_sizes(namespace_name):
|
def cache_namespace_repository_sizes(namespace_name):
|
||||||
namespace = user.get_user_or_org(namespace_name)
|
namespace = user.get_user_or_org(namespace_name)
|
||||||
now_ms = get_epoch_timestamp_ms()
|
now_ms = get_epoch_timestamp_ms()
|
||||||
@ -352,19 +253,6 @@ def cache_namespace_repository_sizes(namespace_name):
|
|||||||
fields=[RepositorySize.repository_id, RepositorySize.size_bytes],
|
fields=[RepositorySize.repository_id, RepositorySize.size_bytes],
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
output = []
|
|
||||||
|
|
||||||
for size in namespace_repo_sizes.dicts():
|
|
||||||
output.append(
|
|
||||||
{
|
|
||||||
"repository_name": size["repository_name"],
|
|
||||||
"repository_id": size["repository_id"],
|
|
||||||
"repository_size": str(size["repository_size"]),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(output)
|
|
||||||
|
|
||||||
|
|
||||||
def get_namespace_size(namespace_name):
|
def get_namespace_size(namespace_name):
|
||||||
namespace = user.get_user_or_org(namespace_name)
|
namespace = user.get_user_or_org(namespace_name)
|
||||||
@ -388,33 +276,51 @@ def get_namespace_size(namespace_name):
|
|||||||
return namespace_size
|
return namespace_size
|
||||||
|
|
||||||
|
|
||||||
def get_repo_quota_for_view(repo_id, namespace):
|
def get_repo_quota_for_view(namespace_name, repo_name):
|
||||||
repo_quota = model.repository.get_repository_size_and_cache(repo_id).get("repository_size", 0)
|
repository_ref = model.repository.get_repository(namespace_name, repo_name)
|
||||||
namespace_quota = get_namespace_quota(namespace)
|
if not repository_ref:
|
||||||
percent_consumed = None
|
return None
|
||||||
if namespace_quota:
|
|
||||||
percent_consumed = str(round((repo_quota / namespace_quota.limit_bytes) * 100, 2))
|
|
||||||
|
|
||||||
return {
|
quotas = get_namespace_quota_list(repository_ref.namespace_user.username)
|
||||||
"percent_consumed": percent_consumed,
|
if not quotas:
|
||||||
"quota_bytes": repo_quota,
|
return {
|
||||||
}
|
"quota_bytes": None,
|
||||||
|
"configured_quota": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Currently only one quota per namespace is supported
|
||||||
|
quota = quotas[0]
|
||||||
|
configured_namespace_quota = quota.limit_bytes
|
||||||
|
|
||||||
def get_org_quota_for_view(namespace):
|
repo_size = model.repository.get_repository_size_and_cache(repository_ref.id).get(
|
||||||
namespace_quota_consumed = get_namespace_size(namespace) or 0
|
"repository_size", 0
|
||||||
configured_namespace_quota = get_namespace_quota(namespace)
|
|
||||||
configured_namespace_quota = (
|
|
||||||
configured_namespace_quota.limit_bytes if configured_namespace_quota else None
|
|
||||||
)
|
)
|
||||||
percent_consumed = None
|
|
||||||
if configured_namespace_quota:
|
|
||||||
percent_consumed = str(
|
|
||||||
round((namespace_quota_consumed / configured_namespace_quota) * 100, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"percent_consumed": percent_consumed,
|
"quota_bytes": repo_size,
|
||||||
"quota_bytes": str(namespace_quota_consumed),
|
"configured_quota": configured_namespace_quota,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_quota_for_view(namespace_name):
|
||||||
|
cache_namespace_repository_sizes(namespace_name)
|
||||||
|
|
||||||
|
namespace_user = model.user.get_user_or_org(namespace_name)
|
||||||
|
quotas = get_namespace_quota_list(namespace_user.username)
|
||||||
|
if not quotas:
|
||||||
|
return {
|
||||||
|
"quota_bytes": None,
|
||||||
|
"configured_quota": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Currently only one quota per namespace is supported
|
||||||
|
quota = quotas[0]
|
||||||
|
configured_namespace_quota = quota.limit_bytes
|
||||||
|
|
||||||
|
namespace_quota_consumed = get_namespace_size(namespace_name)
|
||||||
|
namespace_quota_consumed = int(namespace_quota_consumed) if namespace_quota_consumed else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"quota_bytes": namespace_quota_consumed,
|
||||||
"configured_quota": configured_namespace_quota,
|
"configured_quota": configured_namespace_quota,
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,8 @@ def test_create_quota(initialized_db):
|
|||||||
limit_bytes = 2048
|
limit_bytes = 2048
|
||||||
|
|
||||||
new_org = create_org(user_name, user_email, org_name, org_email)
|
new_org = create_org(user_name, user_email, org_name, org_email)
|
||||||
new_quota = namespacequota.create_namespace_quota(org_name, limit_bytes)
|
new_quota = namespacequota.create_namespace_quota(new_org, limit_bytes)
|
||||||
|
|
||||||
assert new_quota.limit_bytes == limit_bytes
|
assert new_quota.limit_bytes == limit_bytes
|
||||||
assert new_quota.namespace_id.id == new_org.id
|
assert new_quota.namespace == new_org
|
||||||
|
assert new_quota.namespace.id == new_org.id
|
||||||
|
@ -1291,7 +1291,9 @@ def _delete_user_linked_data(user):
|
|||||||
trigger.delete_instance(recursive=True, delete_nullable=False)
|
trigger.delete_instance(recursive=True, delete_nullable=False)
|
||||||
|
|
||||||
with db_transaction():
|
with db_transaction():
|
||||||
namespacequota.delete_namespace_quota(user.username)
|
quotas = namespacequota.get_namespace_quota_list(user.username)
|
||||||
|
for quota in quotas:
|
||||||
|
namespacequota.delete_namespace_quota(quota)
|
||||||
|
|
||||||
# Delete any mirrors with robots owned by this user.
|
# Delete any mirrors with robots owned by this user.
|
||||||
with db_transaction():
|
with db_transaction():
|
||||||
|
@ -131,7 +131,24 @@ def swagger_route_data(include_internal=False, compact=False):
|
|||||||
logger.debug("Unable to find method for %s in class %s", method_name, view_class)
|
logger.debug("Unable to find method for %s in class %s", method_name, view_class)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
operationId = method_metadata(method, "nickname")
|
_operationId = method_metadata(method, "nickname")
|
||||||
|
|
||||||
|
if isinstance(_operationId, list):
|
||||||
|
operationId = None
|
||||||
|
for oid in _operationId:
|
||||||
|
if oid in operationIds:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
operationId = oid
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
if operationId is None:
|
||||||
|
raise Exception("Duplicate operation Id: %s" % operationId)
|
||||||
|
|
||||||
|
else:
|
||||||
|
operationId = _operationId
|
||||||
|
|
||||||
operation_swagger = {
|
operation_swagger = {
|
||||||
"operationId": operationId,
|
"operationId": operationId,
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
Manage organizations, members and OAuth applications.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
@ -13,7 +9,9 @@ from auth.permissions import (
|
|||||||
OrganizationMemberPermission,
|
OrganizationMemberPermission,
|
||||||
UserReadPermission,
|
UserReadPermission,
|
||||||
)
|
)
|
||||||
|
from auth.auth_context import get_authenticated_user
|
||||||
from data import model
|
from data import model
|
||||||
|
from data.database import QuotaTypes
|
||||||
from data.model import config
|
from data.model import config
|
||||||
from endpoints.api import (
|
from endpoints.api import (
|
||||||
resource,
|
resource,
|
||||||
@ -22,45 +20,45 @@ from endpoints.api import (
|
|||||||
validate_json_request,
|
validate_json_request,
|
||||||
request_error,
|
request_error,
|
||||||
require_user_admin,
|
require_user_admin,
|
||||||
|
require_scope,
|
||||||
show_if,
|
show_if,
|
||||||
|
log_action,
|
||||||
)
|
)
|
||||||
from endpoints.exception import InvalidToken, Unauthorized
|
from endpoints.exception import InvalidToken, Unauthorized, NotFound
|
||||||
|
from auth import scopes
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def quota_view(orgname: str, quota, quota_limit_types):
|
def quota_view(quota):
|
||||||
|
quota_limits = list(model.namespacequota.get_namespace_quota_limit_list(quota))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"orgname": orgname,
|
"id": quota.id,
|
||||||
"limit_bytes": quota.limit_bytes if quota else None,
|
"limit_bytes": quota.limit_bytes,
|
||||||
"quota_limit_types": quota_limit_types,
|
"limits": [limit_view(limit) for limit in quota_limits],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def quota_limit_view(orgname: str, quota_limit):
|
def limit_view(limit):
|
||||||
return {
|
return {
|
||||||
"percent_of_limit": quota_limit["percent_of_limit"],
|
"id": limit.id,
|
||||||
"limit_type": {
|
"type": limit.quota_type.name,
|
||||||
"name": quota_limit["name"],
|
"limit_percent": limit.percent_of_limit,
|
||||||
"quota_limit_id": quota_limit["id"],
|
|
||||||
"quota_type_id": quota_limit["type_id"],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def namespace_size_view(orgname: str, repo):
|
def get_quota(namespace_name, quota_id):
|
||||||
return {
|
quota = model.namespacequota.get_namespace_quota(namespace_name, quota_id)
|
||||||
"orgname": orgname,
|
if quota is None:
|
||||||
"repository_name": repo.name,
|
raise NotFound()
|
||||||
"repository_size": repo.repositorysize.size_bytes,
|
return quota
|
||||||
"repository_id": repo.id,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/namespacequota/<namespace>/quota")
|
@resource("/v1/organization/<orgname>/quota")
|
||||||
@show_if(features.QUOTA_MANAGEMENT)
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
class OrganizationQuota(ApiResource):
|
class OrganizationQuotaList(ApiResource):
|
||||||
|
|
||||||
schemas = {
|
schemas = {
|
||||||
"NewOrgQuota": {
|
"NewOrgQuota": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -75,252 +73,323 @@ class OrganizationQuota(ApiResource):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@nickname("getNamespaceQuota")
|
@nickname("listOrganizationQuota")
|
||||||
def get(self, namespace):
|
def get(self, orgname):
|
||||||
orgperm = OrganizationMemberPermission(namespace)
|
orgperm = OrganizationMemberPermission(orgname)
|
||||||
userperm = UserReadPermission(namespace)
|
if not orgperm.can():
|
||||||
|
|
||||||
if not orgperm.can() and not userperm.can():
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
quota = model.namespacequota.get_namespace_quota(namespace)
|
try:
|
||||||
quota_limit_types = model.namespacequota.get_namespace_limit_types()
|
org = model.organization.get_organization(orgname)
|
||||||
return quota_view(namespace, quota, quota_limit_types)
|
except model.InvalidOrganizationException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
@nickname("createNamespaceQuota")
|
quotas = model.namespacequota.get_namespace_quota_list(orgname)
|
||||||
|
|
||||||
|
return [quota_view(quota) for quota in quotas]
|
||||||
|
|
||||||
|
@nickname("createOrganizationQuota")
|
||||||
@validate_json_request("NewOrgQuota")
|
@validate_json_request("NewOrgQuota")
|
||||||
def post(self, namespace):
|
def post(self, orgname):
|
||||||
"""
|
"""
|
||||||
Create a new organization quota.
|
Create a new organization quota.
|
||||||
"""
|
"""
|
||||||
orgperm = AdministerOrganizationPermission(namespace)
|
orgperm = AdministerOrganizationPermission(orgname)
|
||||||
superperm = SuperUserPermission()
|
|
||||||
|
|
||||||
if not superperm.can():
|
if not features.SUPER_USERS or not SuperUserPermission().can():
|
||||||
if orgperm.can():
|
if (
|
||||||
if config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES") != 0:
|
not orgperm.can()
|
||||||
raise Unauthorized()
|
or not config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES") != 0
|
||||||
else:
|
):
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
quota_data = request.get_json()
|
||||||
|
limit_bytes = quota_data["limit_bytes"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
org = model.organization.get_organization(orgname)
|
||||||
|
except model.InvalidOrganizationException:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
# Currently only supporting one quota definition per namespace
|
||||||
|
quotas = model.namespacequota.get_namespace_quota_list(orgname)
|
||||||
|
if quotas:
|
||||||
|
raise request_error(message="Organization quota for '%s' already exists" % orgname)
|
||||||
|
|
||||||
|
try:
|
||||||
|
model.namespacequota.create_namespace_quota(org, limit_bytes)
|
||||||
|
return "Created", 201
|
||||||
|
except model.DataModelException as ex:
|
||||||
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
|
|
||||||
|
@resource("/v1/organization/<orgname>/quota/<quota_id>")
|
||||||
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
|
class OrganizationQuota(ApiResource):
|
||||||
|
schemas = {
|
||||||
|
"UpdateOrgQuota": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Description of a new organization quota",
|
||||||
|
"properties": {
|
||||||
|
"limit_bytes": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of bytes the organization is allowed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname("getOrganizationQuota")
|
||||||
|
def get(self, orgname, quota_id):
|
||||||
|
orgperm = OrganizationMemberPermission(orgname)
|
||||||
|
if not orgperm.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
quota = get_quota(orgname, quota_id)
|
||||||
|
|
||||||
|
return quota_view(quota)
|
||||||
|
|
||||||
|
@nickname("changeOrganizationQuota")
|
||||||
|
@validate_json_request("UpdateOrgQuota")
|
||||||
|
def put(self, orgname, quota_id):
|
||||||
|
orgperm = AdministerOrganizationPermission(orgname)
|
||||||
|
|
||||||
|
if not features.SUPER_USERS or not SuperUserPermission().can():
|
||||||
|
if not orgperm.can():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
quota_data = request.get_json()
|
quota_data = request.get_json()
|
||||||
|
|
||||||
quota = model.namespacequota.get_namespace_quota(namespace)
|
quota = get_quota(orgname, quota_id)
|
||||||
|
|
||||||
if quota is not None:
|
|
||||||
msg = "quota already exists"
|
|
||||||
raise request_error(message=msg)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
newquota = model.namespacequota.create_namespace_quota(
|
if "limit_bytes" in quota_data:
|
||||||
name=namespace, limit_bytes=quota_data["limit_bytes"]
|
limit_bytes = quota_data["limit_bytes"]
|
||||||
)
|
model.namespacequota.update_namespace_quota_size(quota, limit_bytes)
|
||||||
if newquota is not None:
|
|
||||||
return "Created", 201
|
|
||||||
else:
|
|
||||||
raise request_error("Quota Failed to Create")
|
|
||||||
except model.DataModelException as ex:
|
except model.DataModelException as ex:
|
||||||
raise request_error(exception=ex)
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
@nickname("changeOrganizationQuota")
|
return quota_view(quota)
|
||||||
@validate_json_request("NewOrgQuota")
|
|
||||||
def put(self, namespace):
|
|
||||||
|
|
||||||
superperm = SuperUserPermission()
|
|
||||||
|
|
||||||
if not superperm.can():
|
|
||||||
raise Unauthorized()
|
|
||||||
|
|
||||||
quota_data = request.get_json()
|
|
||||||
|
|
||||||
quota = model.namespacequota.get_namespace_quota(namespace)
|
|
||||||
|
|
||||||
if quota is None:
|
|
||||||
msg = "quota does not exist"
|
|
||||||
raise request_error(message=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
model.namespacequota.change_namespace_quota(namespace, quota_data["limit_bytes"])
|
|
||||||
return "Updated", 201
|
|
||||||
except model.DataModelException as ex:
|
|
||||||
raise request_error(exception=ex)
|
|
||||||
|
|
||||||
@nickname("deleteOrganizationQuota")
|
@nickname("deleteOrganizationQuota")
|
||||||
def delete(self, namespace):
|
def delete(self, orgname, quota_id):
|
||||||
superperm = SuperUserPermission()
|
orgperm = AdministerOrganizationPermission(orgname)
|
||||||
|
|
||||||
if not superperm.can():
|
if not features.SUPER_USERS or not SuperUserPermission().can():
|
||||||
raise Unauthorized()
|
if not orgperm.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
quota = model.namespacequota.get_namespace_quota(namespace)
|
quota = get_quota(orgname, quota_id)
|
||||||
|
|
||||||
if quota is None:
|
# Exceptions by`delete_instance` are unexpected and raised
|
||||||
msg = "quota does not exist"
|
model.namespacequota.delete_namespace_quota(quota)
|
||||||
raise request_error(message=msg)
|
|
||||||
|
|
||||||
try:
|
return "", 204
|
||||||
success = model.namespacequota.delete_namespace_quota(namespace)
|
|
||||||
if success == 1:
|
|
||||||
return "Deleted", 201
|
|
||||||
|
|
||||||
msg = "quota failed to delete"
|
|
||||||
raise request_error(message=msg)
|
|
||||||
except model.DataModelException as ex:
|
|
||||||
raise request_error(exception=ex)
|
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/namespacequota/<namespace>/quotalimits")
|
@resource("/v1/organization/<orgname>/quota/<quota_id>/limit")
|
||||||
@show_if(features.QUOTA_MANAGEMENT)
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
class OrganizationQuotaLimits(ApiResource):
|
class OrganizationQuotaLimitList(ApiResource):
|
||||||
|
|
||||||
schemas = {
|
schemas = {
|
||||||
"NewOrgQuotaLimit": {
|
"NewOrgQuotaLimit": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Description of a new organization quota limit threshold",
|
"description": "Description of a new organization quota limit",
|
||||||
"required": ["percent_of_limit", "quota_type_id"],
|
"required": ["type", "threshold_percent"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"percent_of_limit": {
|
"type": {
|
||||||
"type": "integer",
|
"type": "string",
|
||||||
"description": "Percentage of quota at which to do something",
|
"description": 'Type of quota limit: "Warning" or "Reject"',
|
||||||
},
|
},
|
||||||
"quota_type_id": {
|
"threshold_percent": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Quota type Id",
|
"description": "Quota threshold, in percent of quota",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname("listOrganizationQuotaLimit")
|
||||||
|
def get(self, orgname, quota_id):
|
||||||
|
orgperm = OrganizationMemberPermission(orgname)
|
||||||
|
if not orgperm.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
quota = get_quota(orgname, quota_id)
|
||||||
|
return [
|
||||||
|
limit_view(limit)
|
||||||
|
for limit in model.namespacequota.get_namespace_quota_limit_list(quota)
|
||||||
|
]
|
||||||
|
|
||||||
|
@nickname("createOrganizationQuotaLimit")
|
||||||
|
@validate_json_request("NewOrgQuotaLimit")
|
||||||
|
def post(self, orgname, quota_id):
|
||||||
|
orgperm = AdministerOrganizationPermission(orgname)
|
||||||
|
|
||||||
|
if not features.SUPER_USERS or not SuperUserPermission().can():
|
||||||
|
if (
|
||||||
|
not orgperm.can()
|
||||||
|
or not config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES") != 0
|
||||||
|
):
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
quota_limit_data = request.get_json()
|
||||||
|
quota_type = quota_limit_data["type"]
|
||||||
|
quota_limit_threshold = quota_limit_data["threshold_percent"]
|
||||||
|
|
||||||
|
quota = get_quota(orgname, quota_id)
|
||||||
|
|
||||||
|
quota_limit = model.namespacequota.get_namespace_quota_limit_list(
|
||||||
|
quota,
|
||||||
|
quota_type=quota_type,
|
||||||
|
percent_of_limit=quota_limit_threshold,
|
||||||
|
)
|
||||||
|
|
||||||
|
if quota_limit:
|
||||||
|
msg = "Quota limit already exists"
|
||||||
|
raise request_error(message=msg)
|
||||||
|
|
||||||
|
if quota_limit_data["type"].lower() == "reject" and quota_limit:
|
||||||
|
raise request_error(message="Only one quota limit of type 'Reject' allowed.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
model.namespacequota.create_namespace_quota_limit(
|
||||||
|
quota,
|
||||||
|
quota_type,
|
||||||
|
quota_limit_threshold,
|
||||||
|
)
|
||||||
|
return "Created", 201
|
||||||
|
except model.DataModelException as ex:
|
||||||
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
|
|
||||||
|
@resource("/v1/organization/<orgname>/quota/<quota_id>/limit/<limit_id>")
|
||||||
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
|
class OrganizationQuotaLimit(ApiResource):
|
||||||
|
schemas = {
|
||||||
|
"UpdateOrgQuotaLimit": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Description of changing organization quota limit",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": 'Type of quota limit: "Warning" or "Reject"',
|
||||||
|
},
|
||||||
|
"threshold_percent": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Quota threshold, in percent of quota",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@nickname("getOrganizationQuotaLimit")
|
@nickname("getOrganizationQuotaLimit")
|
||||||
def get(self, namespace):
|
def get(self, orgname, quota_id, limit_id):
|
||||||
orgperm = OrganizationMemberPermission(namespace)
|
orgperm = OrganizationMemberPermission(orgname)
|
||||||
userperm = UserReadPermission(namespace)
|
if not orgperm.can():
|
||||||
|
|
||||||
if not orgperm.can() and not userperm.can():
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
quota_limits = list(model.namespacequota.get_namespace_limits(namespace))
|
quota = get_quota(orgname, quota_id)
|
||||||
|
quota_limit = model.namespacequota.get_namespace_quota_limit(quota, limit_id)
|
||||||
|
if quota_limit is None:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
return {"quota_limits": [quota_limit_view(namespace, limit) for limit in quota_limits]}, 200
|
return limit_view(quota_limit)
|
||||||
|
|
||||||
@nickname("createOrganizationQuotaLimit")
|
@nickname("changeOrganizationQuotaLimit")
|
||||||
@validate_json_request("NewOrgQuotaLimit")
|
@validate_json_request("UpdateOrgQuotaLimit")
|
||||||
def post(self, namespace):
|
def put(self, orgname, quota_id, limit_id):
|
||||||
"""
|
orgperm = AdministerOrganizationPermission(orgname)
|
||||||
Create a new organization quota.
|
|
||||||
"""
|
|
||||||
|
|
||||||
orgperm = AdministerOrganizationPermission(namespace)
|
if not features.SUPER_USERS or not SuperUserPermission().can():
|
||||||
superperm = SuperUserPermission()
|
if not orgperm.can():
|
||||||
|
|
||||||
if not superperm.can():
|
|
||||||
if orgperm.can():
|
|
||||||
if config.app_config.get("DEFAULT_SYSTEM_REJECT_QUOTA_BYTES") != 0:
|
|
||||||
raise Unauthorized()
|
|
||||||
else:
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
quota_limit_data = request.get_json()
|
quota_limit_data = request.get_json()
|
||||||
quota = model.namespacequota.get_namespace_limit(
|
|
||||||
namespace, quota_limit_data["quota_type_id"], quota_limit_data["percent_of_limit"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if quota is not None:
|
quota = get_quota(orgname, quota_id)
|
||||||
msg = "quota limit already exists"
|
quota_limit = model.namespacequota.get_namespace_quota_limit(quota, limit_id)
|
||||||
raise request_error(message=msg)
|
if quota_limit is None:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
reject_quota = model.namespacequota.get_namespace_reject_limit(namespace)
|
if "type" in quota_limit_data:
|
||||||
if reject_quota is not None and model.namespacequota.is_reject_limit_type(
|
new_type = quota_limit_data["type"]
|
||||||
quota_limit_data["quota_type_id"]
|
model.namespacequota.update_namespace_quota_limit_type(quota_limit, new_type)
|
||||||
):
|
if "threshold_percent" in quota_limit_data:
|
||||||
msg = "You can only have one Reject type of quota limit"
|
new_threshold = quota_limit_data["threshold_percent"]
|
||||||
raise request_error(message=msg)
|
model.namespacequota.update_namespace_quota_limit_threshold(quota_limit, new_threshold)
|
||||||
|
|
||||||
try:
|
return quota_view(quota)
|
||||||
model.namespacequota.create_namespace_limit(
|
|
||||||
orgname=namespace,
|
|
||||||
percent_of_limit=quota_limit_data["percent_of_limit"],
|
|
||||||
quota_type_id=quota_limit_data["quota_type_id"],
|
|
||||||
)
|
|
||||||
return "Created", 201
|
|
||||||
except model.DataModelException as ex:
|
|
||||||
raise request_error(exception=ex)
|
|
||||||
|
|
||||||
@nickname("changeOrganizationQuotaLimit")
|
|
||||||
@validate_json_request("NewOrgQuotaLimit")
|
|
||||||
def put(self, namespace):
|
|
||||||
|
|
||||||
superperm = SuperUserPermission()
|
|
||||||
|
|
||||||
if not superperm.can():
|
|
||||||
raise Unauthorized()
|
|
||||||
|
|
||||||
quota_limit_data = request.get_json()
|
|
||||||
|
|
||||||
try:
|
|
||||||
quota_limit_id = quota_limit_data["quota_limit_id"]
|
|
||||||
except KeyError:
|
|
||||||
msg = "Must supply quota_limit_id for updates"
|
|
||||||
raise request_error(message=msg)
|
|
||||||
|
|
||||||
quota = model.namespacequota.get_namespace_limit_from_id(namespace, quota_limit_id)
|
|
||||||
|
|
||||||
if quota is None:
|
|
||||||
msg = "quota limit does not exist"
|
|
||||||
raise request_error(message=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
model.namespacequota.change_namespace_quota_limit(
|
|
||||||
namespace,
|
|
||||||
quota_limit_data["percent_of_limit"],
|
|
||||||
quota_limit_data["quota_type_id"],
|
|
||||||
quota_limit_data["quota_limit_id"],
|
|
||||||
)
|
|
||||||
return "Updated", 201
|
|
||||||
except model.DataModelException as ex:
|
|
||||||
raise request_error(exception=ex)
|
|
||||||
|
|
||||||
@nickname("deleteOrganizationQuotaLimit")
|
@nickname("deleteOrganizationQuotaLimit")
|
||||||
def delete(self, namespace):
|
def delete(self, orgname, quota_id, limit_id):
|
||||||
|
orgperm = AdministerOrganizationPermission(orgname)
|
||||||
|
|
||||||
superperm = SuperUserPermission()
|
if not features.SUPER_USERS or not SuperUserPermission().can():
|
||||||
|
if not orgperm.can():
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
if not superperm.can():
|
quota = get_quota(orgname, quota_id)
|
||||||
raise Unauthorized()
|
quota_limit = model.namespacequota.get_namespace_quota_limit(quota, limit_id)
|
||||||
|
if quota_limit is None:
|
||||||
quota_limit_id = request.args.get("quota_limit_id", None)
|
raise NotFound()
|
||||||
if quota_limit_id is None:
|
|
||||||
msg = "Bad request to delete quota limit. Missing quota limit identifier."
|
|
||||||
raise request_error(message=msg)
|
|
||||||
|
|
||||||
quota = model.namespacequota.get_namespace_limit_from_id(namespace, quota_limit_id)
|
|
||||||
|
|
||||||
if quota is None:
|
|
||||||
msg = "quota does not exist"
|
|
||||||
raise request_error(message=msg)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = model.namespacequota.delete_namespace_quota_limit(namespace, quota_limit_id)
|
# Exceptions by`delete_instance` are unexpected and raised
|
||||||
if success == 1:
|
model.namespacequota.delete_namespace_quota_limit(quota_limit)
|
||||||
return "Deleted", 201
|
return "", 204
|
||||||
|
|
||||||
msg = "quota failed to delete"
|
|
||||||
raise request_error(message=msg)
|
|
||||||
except model.DataModelException as ex:
|
except model.DataModelException as ex:
|
||||||
raise request_error(exception=ex)
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/namespacequota/<namespace>/quotareport")
|
@resource("/v1/user/quota")
|
||||||
@show_if(features.QUOTA_MANAGEMENT)
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
class OrganizationQuotaReport(ApiResource):
|
class UserQuotaList(ApiResource):
|
||||||
@nickname("getOrganizationSizeReporting")
|
@require_user_admin
|
||||||
def get(self, namespace):
|
@nickname("listUserQuota")
|
||||||
orgperm = OrganizationMemberPermission(namespace)
|
def get(self):
|
||||||
userperm = UserReadPermission(namespace)
|
parent = get_authenticated_user()
|
||||||
|
user_quotas = model.namespacequota.get_namespace_quota_list(parent.username)
|
||||||
|
|
||||||
if not orgperm.can() and not userperm.can():
|
return [quota_view(quota) for quota in user_quotas]
|
||||||
raise Unauthorized()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"response": model.namespacequota.get_namespace_repository_sizes_and_cache(namespace)
|
@resource("/v1/user/quota/<quota_id>")
|
||||||
}, 200
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
|
class UserQuota(ApiResource):
|
||||||
|
@require_user_admin
|
||||||
|
@nickname("getUserQuota")
|
||||||
|
def get(self, quota_id):
|
||||||
|
parent = get_authenticated_user()
|
||||||
|
quota = get_quota(parent.username, quota_id)
|
||||||
|
|
||||||
|
return quota_view(quota)
|
||||||
|
|
||||||
|
|
||||||
|
@resource("/v1/user/quota/<quota_id>/limit")
|
||||||
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
|
class UserQuotaLimitList(ApiResource):
|
||||||
|
@require_user_admin
|
||||||
|
@nickname("listUserQuotaLimit")
|
||||||
|
def get(self, quota_id):
|
||||||
|
parent = get_authenticated_user()
|
||||||
|
quota = get_quota(parent.username, quota_id)
|
||||||
|
|
||||||
|
return [
|
||||||
|
limit_view(limit)
|
||||||
|
for limit in model.namespacequota.get_namespace_quota_limit_list(quota)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@resource("/v1/user/quota/<quota_id>/limit/<limit_id>")
|
||||||
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
|
class UserQuotaLimit(ApiResource):
|
||||||
|
@require_user_admin
|
||||||
|
@nickname("getUserQuotaLimit")
|
||||||
|
def get(self, quota_id, limit_id):
|
||||||
|
parent = get_authenticated_user()
|
||||||
|
quota = get_quota(parent.username, quota_id)
|
||||||
|
quota_limit = model.namespacequota.get_namespace_quota_limit(quota, limit_id)
|
||||||
|
if quota_limit is None:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
return quota_view(quota)
|
||||||
|
@ -53,6 +53,24 @@ from proxy import Proxy, UpstreamRegistryError
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def quota_view(quota):
|
||||||
|
quota_limits = list(model.namespacequota.get_namespace_quota_limit_list(quota))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": quota.id, # Generate uuid instead?
|
||||||
|
"limit_bytes": quota.limit_bytes,
|
||||||
|
"limits": [limit_view(limit) for limit in quota_limits],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def limit_view(limit):
|
||||||
|
return {
|
||||||
|
"id": limit.id,
|
||||||
|
"type": limit.quota_type.name,
|
||||||
|
"limit_percent": limit.percent_of_limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def team_view(orgname, team):
|
def team_view(orgname, team):
|
||||||
return {
|
return {
|
||||||
"name": team.name,
|
"name": team.name,
|
||||||
@ -66,7 +84,7 @@ def team_view(orgname, team):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def org_view(o, teams, quota=None):
|
def org_view(o, teams):
|
||||||
is_admin = AdministerOrganizationPermission(o.username).can()
|
is_admin = AdministerOrganizationPermission(o.username).can()
|
||||||
is_member = OrganizationMemberPermission(o.username).can()
|
is_member = OrganizationMemberPermission(o.username).can()
|
||||||
|
|
||||||
@ -76,7 +94,6 @@ def org_view(o, teams, quota=None):
|
|||||||
"avatar": avatar.get_data_for_user(o),
|
"avatar": avatar.get_data_for_user(o),
|
||||||
"is_admin": is_admin,
|
"is_admin": is_admin,
|
||||||
"is_member": is_member,
|
"is_member": is_member,
|
||||||
"quota": quota,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if teams is not None:
|
if teams is not None:
|
||||||
@ -90,6 +107,11 @@ def org_view(o, teams, quota=None):
|
|||||||
view["tag_expiration_s"] = o.removed_tag_expiration_s
|
view["tag_expiration_s"] = o.removed_tag_expiration_s
|
||||||
view["is_free_account"] = o.stripe_id is None
|
view["is_free_account"] = o.stripe_id is None
|
||||||
|
|
||||||
|
if features.QUOTA_MANAGEMENT:
|
||||||
|
quotas = model.namespacequota.get_namespace_quota_list(o.username)
|
||||||
|
view["quotas"] = [quota_view(quota) for quota in quotas] if quotas else []
|
||||||
|
view["quota_report"] = model.namespacequota.get_quota_for_view(o.username)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
@ -218,15 +240,11 @@ class Organization(ApiResource):
|
|||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
teams = None
|
teams = None
|
||||||
quota = None
|
|
||||||
if OrganizationMemberPermission(orgname).can():
|
if OrganizationMemberPermission(orgname).can():
|
||||||
has_syncing = features.TEAM_SYNCING and bool(authentication.federated_service)
|
has_syncing = features.TEAM_SYNCING and bool(authentication.federated_service)
|
||||||
teams = model.team.get_teams_within_org(org, has_syncing)
|
teams = model.team.get_teams_within_org(org, has_syncing)
|
||||||
|
|
||||||
if features.QUOTA_MANAGEMENT:
|
return org_view(org, teams)
|
||||||
quota = model.namespacequota.get_org_quota_for_view(org.username)
|
|
||||||
|
|
||||||
return org_view(org, teams, quota)
|
|
||||||
|
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname("changeOrganizationDetails")
|
@nickname("changeOrganizationDetails")
|
||||||
|
@ -211,12 +211,6 @@ class RepositoryList(ApiResource):
|
|||||||
type=truthy_bool,
|
type=truthy_bool,
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
@query_param(
|
|
||||||
"quota",
|
|
||||||
"Whether to include the repository's consumed quota.",
|
|
||||||
type=truthy_bool,
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
@query_param("repo_kind", "The kind of repositories to return", type=str, default="image")
|
@query_param("repo_kind", "The kind of repositories to return", type=str, default="image")
|
||||||
@page_support()
|
@page_support()
|
||||||
def get(self, page_token, parsed_args):
|
def get(self, page_token, parsed_args):
|
||||||
@ -237,7 +231,6 @@ class RepositoryList(ApiResource):
|
|||||||
username = user.username if user else None
|
username = user.username if user else None
|
||||||
last_modified = parsed_args["last_modified"]
|
last_modified = parsed_args["last_modified"]
|
||||||
popularity = parsed_args["popularity"]
|
popularity = parsed_args["popularity"]
|
||||||
quota = parsed_args["quota"]
|
|
||||||
|
|
||||||
if parsed_args["starred"] and not username:
|
if parsed_args["starred"] and not username:
|
||||||
# No repositories should be returned, as there is no user.
|
# No repositories should be returned, as there is no user.
|
||||||
@ -253,7 +246,6 @@ class RepositoryList(ApiResource):
|
|||||||
page_token,
|
page_token,
|
||||||
last_modified,
|
last_modified,
|
||||||
popularity,
|
popularity,
|
||||||
quota,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"repositories": [repo.to_dict() for repo in repos]}, next_page_token
|
return {"repositories": [repo.to_dict() for repo in repos]}, next_page_token
|
||||||
|
@ -6,6 +6,7 @@ from six import add_metaclass
|
|||||||
|
|
||||||
import features
|
import features
|
||||||
from data.database import RepositoryState
|
from data.database import RepositoryState
|
||||||
|
from data import model
|
||||||
from endpoints.api import format_date
|
from endpoints.api import format_date
|
||||||
|
|
||||||
|
|
||||||
@ -28,7 +29,6 @@ class RepositoryBaseElement(
|
|||||||
"should_is_starred",
|
"should_is_starred",
|
||||||
"is_free_account",
|
"is_free_account",
|
||||||
"state",
|
"state",
|
||||||
"quota",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
@ -45,7 +45,6 @@ class RepositoryBaseElement(
|
|||||||
:type should_last_modified: boolean
|
:type should_last_modified: boolean
|
||||||
:type should_popularity: boolean
|
:type should_popularity: boolean
|
||||||
:type should_is_starred: boolean
|
:type should_is_starred: boolean
|
||||||
:type: quota: dictionary
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@ -56,9 +55,13 @@ class RepositoryBaseElement(
|
|||||||
"is_public": self.is_public,
|
"is_public": self.is_public,
|
||||||
"kind": self.kind_name,
|
"kind": self.kind_name,
|
||||||
"state": self.state.name if self.state is not None else None,
|
"state": self.state.name if self.state is not None else None,
|
||||||
"quota": self.quota if features.QUOTA_MANAGEMENT else None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if features.QUOTA_MANAGEMENT:
|
||||||
|
repo["quota_report"] = model.namespacequota.get_repo_quota_for_view(
|
||||||
|
self.namespace_name, self.repository_name
|
||||||
|
)
|
||||||
|
|
||||||
if self.should_last_modified:
|
if self.should_last_modified:
|
||||||
repo["last_modified"] = self.last_modified
|
repo["last_modified"] = self.last_modified
|
||||||
|
|
||||||
|
@ -90,7 +90,6 @@ class PreOCIModel(RepositoryDataInterface):
|
|||||||
page_token,
|
page_token,
|
||||||
last_modified,
|
last_modified,
|
||||||
popularity,
|
popularity,
|
||||||
quota,
|
|
||||||
):
|
):
|
||||||
next_page_token = None
|
next_page_token = None
|
||||||
|
|
||||||
@ -134,7 +133,6 @@ class PreOCIModel(RepositoryDataInterface):
|
|||||||
# and/or last modified.
|
# and/or last modified.
|
||||||
last_modified_map = {}
|
last_modified_map = {}
|
||||||
action_sum_map = {}
|
action_sum_map = {}
|
||||||
quota_map = {}
|
|
||||||
if last_modified or popularity:
|
if last_modified or popularity:
|
||||||
repository_refs = [RepositoryReference.for_id(repo.rid) for repo in repos]
|
repository_refs = [RepositoryReference.for_id(repo.rid) for repo in repos]
|
||||||
repository_ids = [repo.rid for repo in repos]
|
repository_ids = [repo.rid for repo in repos]
|
||||||
@ -151,12 +149,6 @@ class PreOCIModel(RepositoryDataInterface):
|
|||||||
if popularity:
|
if popularity:
|
||||||
action_sum_map = model.log.get_repositories_action_sums(repository_ids)
|
action_sum_map = model.log.get_repositories_action_sums(repository_ids)
|
||||||
|
|
||||||
if features.QUOTA_MANAGEMENT and quota:
|
|
||||||
for repo_id in repository_ids:
|
|
||||||
quota_map[repo_id] = model.namespacequota.get_repo_quota_for_view(
|
|
||||||
repo_id, namespace
|
|
||||||
)
|
|
||||||
|
|
||||||
# Collect the IDs of the repositories that are starred for the user, so we can mark them
|
# Collect the IDs of the repositories that are starred for the user, so we can mark them
|
||||||
# in the returned results.
|
# in the returned results.
|
||||||
star_set = set()
|
star_set = set()
|
||||||
@ -182,7 +174,6 @@ class PreOCIModel(RepositoryDataInterface):
|
|||||||
username,
|
username,
|
||||||
None,
|
None,
|
||||||
repo.state,
|
repo.state,
|
||||||
quota_map.get(repo.rid),
|
|
||||||
)
|
)
|
||||||
for repo in repos
|
for repo in repos
|
||||||
],
|
],
|
||||||
@ -242,7 +233,6 @@ class PreOCIModel(RepositoryDataInterface):
|
|||||||
False,
|
False,
|
||||||
repo.namespace_user.stripe_id is None,
|
repo.namespace_user.stripe_id is None,
|
||||||
repo.state,
|
repo.state,
|
||||||
features.QUOTA_MANAGEMENT is True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if base.kind_name == "application":
|
if base.kind_name == "application":
|
||||||
|
@ -21,7 +21,7 @@ from auth.auth_context import get_authenticated_user
|
|||||||
from auth.permissions import SuperUserPermission
|
from auth.permissions import SuperUserPermission
|
||||||
from data.database import ServiceKeyApprovalType
|
from data.database import ServiceKeyApprovalType
|
||||||
from data.logs_model import logs_model
|
from data.logs_model import logs_model
|
||||||
from data.model import namespacequota
|
from data.model import user, namespacequota, InvalidNamespaceQuota, DataModelException
|
||||||
from endpoints.api import (
|
from endpoints.api import (
|
||||||
ApiResource,
|
ApiResource,
|
||||||
nickname,
|
nickname,
|
||||||
@ -43,7 +43,9 @@ from endpoints.api import (
|
|||||||
Unauthorized,
|
Unauthorized,
|
||||||
InvalidResponse,
|
InvalidResponse,
|
||||||
)
|
)
|
||||||
|
from endpoints.api import request_error
|
||||||
from endpoints.api.build import get_logs_or_log_url
|
from endpoints.api.build import get_logs_or_log_url
|
||||||
|
from endpoints.api.namespacequota import quota_view, limit_view, get_quota
|
||||||
from endpoints.api.superuser_models_pre_oci import (
|
from endpoints.api.superuser_models_pre_oci import (
|
||||||
pre_oci_model,
|
pre_oci_model,
|
||||||
ServiceKeyDoesNotExist,
|
ServiceKeyDoesNotExist,
|
||||||
@ -214,33 +216,108 @@ class SuperUserOrganizationList(ApiResource):
|
|||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/superuser/quota/")
|
@resource(
|
||||||
|
"/v1/superuser/users/<namespace>/quota/",
|
||||||
|
"/v1/superuser/organization/<namespace>/quota/",
|
||||||
|
)
|
||||||
@internal_only
|
@internal_only
|
||||||
@show_if(features.SUPER_USERS)
|
@show_if(features.SUPER_USERS)
|
||||||
@show_if(features.QUOTA_MANAGEMENT)
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
class SuperUserOrganizationQuotaReport(ApiResource):
|
class SuperUserUserQuotaList(ApiResource):
|
||||||
"""
|
|
||||||
Resource for listing organizations in the system.
|
schemas = {
|
||||||
"""
|
"NewNamespaceQuota": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Description of a new organization quota",
|
||||||
|
"required": ["limit_bytes"],
|
||||||
|
"properties": {
|
||||||
|
"limit_bytes": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of bytes the organization is allowed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname("allOrganizationQuotaReport")
|
@nickname(["createUserQuotaSuperUser", "createOrganizationQuotaSuperUser"])
|
||||||
@require_scope(scopes.SUPERUSER)
|
@require_scope(scopes.SUPERUSER)
|
||||||
def get(self):
|
@validate_json_request("NewNamespaceQuota")
|
||||||
"""
|
def post(self, namespace):
|
||||||
Returns a list of all organizations in the system.
|
|
||||||
"""
|
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
return {
|
quota_data = request.get_json()
|
||||||
"organizations": [
|
limit_bytes = quota_data["limit_bytes"]
|
||||||
{
|
|
||||||
"organization": org.username,
|
namespace_user = user.get_user_or_org(namespace)
|
||||||
"size": namespacequota.get_namespace_size(org.username),
|
quotas = namespacequota.get_namespace_quota_list(namespace_user.username)
|
||||||
}
|
|
||||||
for org in pre_oci_model.get_organizations()
|
if quotas:
|
||||||
]
|
raise request_error(message="Quota for '%s' already exists" % namespace)
|
||||||
}
|
|
||||||
|
try:
|
||||||
|
newquota = namespacequota.create_namespace_quota(namespace_user, limit_bytes)
|
||||||
|
return "Created", 201
|
||||||
|
except DataModelException as ex:
|
||||||
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@resource(
|
||||||
|
"/v1/superuser/users/<namespace>/quota/<quota_id>",
|
||||||
|
"/v1/superuser/organization/<namespace>/quota/<quota_id>",
|
||||||
|
)
|
||||||
|
@internal_only
|
||||||
|
@show_if(features.SUPER_USERS)
|
||||||
|
@show_if(features.QUOTA_MANAGEMENT)
|
||||||
|
class SuperUserUserQuota(ApiResource):
|
||||||
|
|
||||||
|
schemas = {
|
||||||
|
"UpdateNamespaceQuota": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Description of a new organization quota",
|
||||||
|
"properties": {
|
||||||
|
"limit_bytes": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of bytes the organization is allowed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_fresh_login
|
||||||
|
@verify_not_prod
|
||||||
|
@nickname(["changeUserQuotaSuperUser", "changeOrganizationQuotaSuperUser"])
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
|
@validate_json_request("UpdateNamespaceQuota")
|
||||||
|
def put(self, namespace, quota_id):
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
quota_data = request.get_json()
|
||||||
|
|
||||||
|
namespace_user = user.get_user_or_org(namespace)
|
||||||
|
quota = get_quota(namespace_user.username, quota_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if "limit_bytes" in quota_data:
|
||||||
|
limit_bytes = quota_data["limit_bytes"]
|
||||||
|
model.namespacequota.update_namespace_quota_size(quota, limit_bytes)
|
||||||
|
except model.DataModelException as ex:
|
||||||
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
|
return quota_view(quota)
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@nickname(["deleteUserQuotaSuperUser", "deleteOrganizationQuotaSuperUser"])
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
|
def delete(self, namespace, quota_id):
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
namespace_user = user.get_user_or_org(namespace)
|
||||||
|
quota = get_quota(namespace_user.username, quota_id)
|
||||||
|
namespacequota.delete_namespace_quota(quota)
|
||||||
|
|
||||||
|
return "", 204
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@ -23,6 +23,24 @@ def user_view(user):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def quota_view(quota):
|
||||||
|
quota_limits = list(model.namespacequota.get_namespace_quota_limit_list(quota))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": quota.id, # Generate uuid instead?
|
||||||
|
"limit_bytes": quota.limit_bytes,
|
||||||
|
"limits": [limit_view(limit) for limit in quota_limits],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def limit_view(limit):
|
||||||
|
return {
|
||||||
|
"id": limit.id,
|
||||||
|
"type": limit.quota_type.name,
|
||||||
|
"limit_percent": limit.percent_of_limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BuildTrigger(
|
class BuildTrigger(
|
||||||
namedtuple("BuildTrigger", ["trigger", "pull_robot", "can_read", "can_admin", "for_build"])
|
namedtuple("BuildTrigger", ["trigger", "pull_robot", "can_read", "can_admin", "for_build"])
|
||||||
):
|
):
|
||||||
@ -205,7 +223,7 @@ class ServiceKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class User(namedtuple("User", ["username", "email", "verified", "enabled", "robot"])):
|
class User(namedtuple("User", ["username", "email", "verified", "enabled", "robot", "quotas"])):
|
||||||
"""
|
"""
|
||||||
User represents a single user.
|
User represents a single user.
|
||||||
|
|
||||||
@ -227,28 +245,37 @@ class User(namedtuple("User", ["username", "email", "verified", "enabled", "robo
|
|||||||
"super_user": superusers.is_superuser(self.username),
|
"super_user": superusers.is_superuser(self.username),
|
||||||
"enabled": self.enabled,
|
"enabled": self.enabled,
|
||||||
}
|
}
|
||||||
|
if features.QUOTA_MANAGEMENT and self.quotas is not None:
|
||||||
|
user_data["quotas"] = (
|
||||||
|
[quota_view(quota) for quota in self.quotas] if self.quotas else []
|
||||||
|
)
|
||||||
|
user_data["quota_report"] = model.namespacequota.get_quota_for_view(self.username)
|
||||||
|
|
||||||
return user_data
|
return user_data
|
||||||
|
|
||||||
|
|
||||||
class Organization(namedtuple("Organization", ["username", "email"])):
|
class Organization(namedtuple("Organization", ["username", "email", "quotas"])):
|
||||||
"""
|
"""
|
||||||
Organization represents a single org.
|
Organization represents a single org.
|
||||||
|
|
||||||
:type username: string
|
:type username: string
|
||||||
:type email: string
|
:type email: string
|
||||||
|
:type quotas: [UserOrganizationQuota] | None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
d = {
|
||||||
"name": self.username,
|
"name": self.username,
|
||||||
"email": self.email,
|
"email": self.email,
|
||||||
"avatar": avatar.get_data_for_org(self),
|
"avatar": avatar.get_data_for_org(self),
|
||||||
"quota": model.namespacequota.get_org_quota_for_view(self.username)
|
|
||||||
if features.QUOTA_MANAGEMENT
|
|
||||||
else None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if features.QUOTA_MANAGEMENT and self.quotas is not None:
|
||||||
|
d["quotas"] = [quota_view(quota) for quota in self.quotas] if self.quotas else []
|
||||||
|
d["quota_report"] = model.namespacequota.get_quota_for_view(self.username)
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
@add_metaclass(ABCMeta)
|
@add_metaclass(ABCMeta)
|
||||||
class SuperuserDataInterface(object):
|
class SuperuserDataInterface(object):
|
||||||
|
@ -22,10 +22,20 @@ from endpoints.api.superuser_models_interface import (
|
|||||||
from util.request import get_request_ip
|
from util.request import get_request_ip
|
||||||
|
|
||||||
|
|
||||||
|
def _get_namespace_quotas(namespace_user):
|
||||||
|
if not features.QUOTA_MANAGEMENT:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return model.namespacequota.get_namespace_quota_list(namespace_user.username)
|
||||||
|
|
||||||
|
|
||||||
def _create_user(user):
|
def _create_user(user):
|
||||||
if user is None:
|
if user is None:
|
||||||
return None
|
return None
|
||||||
return User(user.username, user.email, user.verified, user.enabled, user.robot)
|
|
||||||
|
quotas = _get_namespace_quotas(user)
|
||||||
|
|
||||||
|
return User(user.username, user.email, user.verified, user.enabled, user.robot, quotas)
|
||||||
|
|
||||||
|
|
||||||
def _create_key(key):
|
def _create_key(key):
|
||||||
@ -163,7 +173,9 @@ class PreOCIModel(SuperuserDataInterface):
|
|||||||
if new_org_name is not None:
|
if new_org_name is not None:
|
||||||
org = model.user.change_username(org.id, new_org_name)
|
org = model.user.change_username(org.id, new_org_name)
|
||||||
|
|
||||||
return Organization(org.username, org.email)
|
quotas = _get_namespace_quotas(org)
|
||||||
|
|
||||||
|
return Organization(org.username, org.email, quotas)
|
||||||
|
|
||||||
def mark_organization_for_deletion(self, name):
|
def mark_organization_for_deletion(self, name):
|
||||||
org = model.organization.get_organization(name)
|
org = model.organization.get_organization(name)
|
||||||
@ -235,7 +247,8 @@ class PreOCIModel(SuperuserDataInterface):
|
|||||||
|
|
||||||
def get_organizations(self):
|
def get_organizations(self):
|
||||||
return [
|
return [
|
||||||
Organization(org.username, org.email) for org in model.organization.get_organizations()
|
Organization(org.username, org.email, _get_namespace_quotas(org))
|
||||||
|
for org in model.organization.get_organizations()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -5522,51 +5522,6 @@ SECURITY_TESTS: List[
|
|||||||
(RepositoryStateResource, "PUT", {"repository": "devtable/simple"}, None, "devtable", 400),
|
(RepositoryStateResource, "PUT", {"repository": "devtable/simple"}, None, "devtable", 400),
|
||||||
(RepositoryStateResource, "PUT", {"repository": "devtable/simple"}, None, "freshuser", 403),
|
(RepositoryStateResource, "PUT", {"repository": "devtable/simple"}, None, "freshuser", 403),
|
||||||
(RepositoryStateResource, "PUT", {"repository": "devtable/simple"}, None, "reader", 403),
|
(RepositoryStateResource, "PUT", {"repository": "devtable/simple"}, None, "reader", 403),
|
||||||
(
|
|
||||||
OrganizationQuota,
|
|
||||||
"POST",
|
|
||||||
{"namespace": "buynlarge"},
|
|
||||||
{"limit_bytes": 1, "bytes_unit": "GB"},
|
|
||||||
None,
|
|
||||||
401,
|
|
||||||
),
|
|
||||||
(OrganizationQuota, "GET", {"namespace": "buynlarge"}, None, None, 401),
|
|
||||||
(
|
|
||||||
OrganizationQuota,
|
|
||||||
"PUT",
|
|
||||||
{"namespace": "buynlarge"},
|
|
||||||
{"limit_bytes": 1, "bytes_unit": "GB"},
|
|
||||||
None,
|
|
||||||
401,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
OrganizationQuotaLimits,
|
|
||||||
"POST",
|
|
||||||
{"namespace": "buynlarge"},
|
|
||||||
{"percent_of_limit": 10, "quota_type_id": 2},
|
|
||||||
None,
|
|
||||||
401,
|
|
||||||
),
|
|
||||||
(OrganizationQuotaLimits, "GET", {"namespace": "buynlarge"}, None, None, 401),
|
|
||||||
(
|
|
||||||
OrganizationQuotaLimits,
|
|
||||||
"PUT",
|
|
||||||
{"namespace": "buynlarge"},
|
|
||||||
{"percent_of_limit": 10, "quota_type_id": 2},
|
|
||||||
None,
|
|
||||||
401,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
OrganizationQuotaLimits,
|
|
||||||
"DELETE",
|
|
||||||
{"namespace": "buynlarge"},
|
|
||||||
{"quota_limit_id": 1},
|
|
||||||
None,
|
|
||||||
401,
|
|
||||||
),
|
|
||||||
(OrganizationQuota, "DELETE", {"namespace": "buynlarge"}, {}, None, 401),
|
|
||||||
(OrganizationQuotaReport, "GET", {"namespace": "buynlarge"}, {}, None, 401),
|
|
||||||
(SuperUserOrganizationQuotaReport, "GET", {"namespace": "buynlarge"}, {}, None, 401),
|
|
||||||
(
|
(
|
||||||
OrganizationProxyCacheConfig,
|
OrganizationProxyCacheConfig,
|
||||||
"GET",
|
"GET",
|
||||||
@ -5671,6 +5626,252 @@ SECURITY_TESTS: List[
|
|||||||
"devtable",
|
"devtable",
|
||||||
202,
|
202,
|
||||||
),
|
),
|
||||||
|
(OrganizationQuotaList, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
|
||||||
|
(OrganizationQuotaList, "GET", {"orgname": "buynlarge"}, None, "randomuser", 403),
|
||||||
|
(OrganizationQuotaList, "GET", {"orgname": "buynlarge"}, None, None, 401),
|
||||||
|
(OrganizationQuotaList, "POST", {"orgname": "buynlarge"}, {"limit_bytes": 200000}, None, 401),
|
||||||
|
(
|
||||||
|
OrganizationQuotaList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge"},
|
||||||
|
{"limit_bytes": 200000},
|
||||||
|
"devtable",
|
||||||
|
400,
|
||||||
|
), # Quota already exists in test db
|
||||||
|
(
|
||||||
|
OrganizationQuotaList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge"},
|
||||||
|
{"limit_bytes": 200000},
|
||||||
|
"randomuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "library"},
|
||||||
|
{"limit_bytes": 200000},
|
||||||
|
"devtable",
|
||||||
|
201,
|
||||||
|
),
|
||||||
|
(OrganizationQuota, "GET", {"orgname": "buynlarge", "quota_id": 1}, None, None, 401),
|
||||||
|
(OrganizationQuota, "GET", {"orgname": "buynlarge", "quota_id": 1}, None, "randomuser", 403),
|
||||||
|
(OrganizationQuota, "GET", {"orgname": "buynlarge", "quota_id": 1}, None, "devtable", 200),
|
||||||
|
(OrganizationQuota, "GET", {"orgname": "buynlarge", "quota_id": 2}, None, "devtable", 404),
|
||||||
|
(OrganizationQuota, "PUT", {"orgname": "buynlarge", "quota_id": 1}, {}, None, 401),
|
||||||
|
(OrganizationQuota, "PUT", {"orgname": "buynlarge", "quota_id": 1}, {}, "randomuser", 403),
|
||||||
|
(OrganizationQuota, "PUT", {"orgname": "buynlarge", "quota_id": 1}, {}, "devtable", 200),
|
||||||
|
(OrganizationQuota, "DELETE", {"orgname": "buynlarge", "quota_id": 1}, None, None, 401),
|
||||||
|
(OrganizationQuota, "DELETE", {"orgname": "buynlarge", "quota_id": 1}, None, "randomuser", 403),
|
||||||
|
(OrganizationQuota, "DELETE", {"orgname": "buynlarge", "quota_id": 1}, None, "devtable", 204),
|
||||||
|
(OrganizationQuotaLimitList, "GET", {"orgname": "buynlarge", "quota_id": 1}, None, None, 401),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimitList,
|
||||||
|
"GET",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1},
|
||||||
|
None,
|
||||||
|
"randomuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimitList,
|
||||||
|
"GET",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimitList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1},
|
||||||
|
{"type": "warning", "threshold_percent": 50},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimitList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1},
|
||||||
|
{"type": "warning", "threshold_percent": 50},
|
||||||
|
"randomuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimitList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1},
|
||||||
|
{"type": "warning", "threshold_percent": 50},
|
||||||
|
"devtable",
|
||||||
|
400,
|
||||||
|
), # Exact same configuration already exists
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimitList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1},
|
||||||
|
{"type": "undfinedtype", "threshold_percent": 60},
|
||||||
|
"devtable",
|
||||||
|
400,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimitList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1},
|
||||||
|
{"type": "warning", "threshold_percent": 60},
|
||||||
|
"devtable",
|
||||||
|
201,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimitList,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1},
|
||||||
|
{"type": "reject", "threshold_percent": 60},
|
||||||
|
"devtable",
|
||||||
|
201,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"GET",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"GET",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
None,
|
||||||
|
"randomuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"GET",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"PUT",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
{"type": "reject", "threshold_percent": 60},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"PUT",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
{"type": "reject", "threshold_percent": 60},
|
||||||
|
"randomuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"PUT",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
{"type": "reject", "threshold_percent": 60},
|
||||||
|
"devtable",
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"PUT",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
{"type": "undefinedtype", "threshold_percent": 60},
|
||||||
|
"devtable",
|
||||||
|
400,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"DELETE",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"DELETE",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
None,
|
||||||
|
"randomuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationQuotaLimit,
|
||||||
|
"DELETE",
|
||||||
|
{"orgname": "buynlarge", "quota_id": 1, "limit_id": 1},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
204,
|
||||||
|
),
|
||||||
|
(UserQuotaList, "GET", {}, None, None, 401),
|
||||||
|
(UserQuotaList, "GET", {}, None, "freshuser", 200),
|
||||||
|
(UserQuotaList, "GET", {}, None, "devtable", 200),
|
||||||
|
(UserQuota, "GET", {"quota_id": 2}, None, "freshuser", 404),
|
||||||
|
(UserQuota, "GET", {"quota_id": 2}, None, "devtable", 404),
|
||||||
|
(UserQuota, "GET", {"quota_id": 2}, None, "randomuser", 200),
|
||||||
|
(UserQuotaLimitList, "GET", {"quota_id": 2}, None, "devtable", 404),
|
||||||
|
(UserQuotaLimitList, "GET", {"quota_id": 2}, None, "randomuser", 200),
|
||||||
|
(UserQuotaLimit, "GET", {"quota_id": 2, "limit_id": 2}, None, "devtable", 404),
|
||||||
|
(UserQuotaLimit, "GET", {"quota_id": 2, "limit_id": 2}, None, "randomuser", 200),
|
||||||
|
(SuperUserUserQuotaList, "POST", {"namespace": "randomuser"}, {"limit_bytes": 5000}, None, 401),
|
||||||
|
(SuperUserUserQuotaList, "POST", {"namespace": "randomuser"}, None, None, 401),
|
||||||
|
(
|
||||||
|
SuperUserUserQuotaList,
|
||||||
|
"POST",
|
||||||
|
{"namespace": "randomuser"},
|
||||||
|
{"limit_bytes": 5000},
|
||||||
|
"freshuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SuperUserUserQuotaList,
|
||||||
|
"POST",
|
||||||
|
{"namespace": "randomuser"},
|
||||||
|
{"limit_bytes": 5000},
|
||||||
|
"devtable",
|
||||||
|
400,
|
||||||
|
), # Quota for this user already exists
|
||||||
|
(
|
||||||
|
SuperUserUserQuotaList,
|
||||||
|
"POST",
|
||||||
|
{"namespace": "freshuser"},
|
||||||
|
{"limit_bytes": 5000},
|
||||||
|
"devtable",
|
||||||
|
201,
|
||||||
|
),
|
||||||
|
(SuperUserUserQuotaList, "POST", {"namespace": "randomuser"}, None, "devtable", 400),
|
||||||
|
(SuperUserUserQuota, "PUT", {"namespace": "randomuser", "quota_id": 2}, {}, "randomuser", 403),
|
||||||
|
(SuperUserUserQuota, "PUT", {"namespace": "randomuser", "quota_id": 2}, {}, "devtable", 200),
|
||||||
|
(
|
||||||
|
SuperUserUserQuota,
|
||||||
|
"DELETE",
|
||||||
|
{"namespace": "randomuser", "quota_id": 2},
|
||||||
|
None,
|
||||||
|
"freshuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SuperUserUserQuota,
|
||||||
|
"DELETE",
|
||||||
|
{"namespace": "randomuser", "quota_id": 2},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
204,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SuperUserUserQuota,
|
||||||
|
"DELETE",
|
||||||
|
{"namespace": "randomuser", "quota_id": 1},
|
||||||
|
None,
|
||||||
|
"devtable",
|
||||||
|
404,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -184,6 +184,11 @@ def user_view(user, previous_username=None):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if features.QUOTA_MANAGEMENT:
|
||||||
|
quotas = model.namespacequota.get_namespace_quota_list(user.username)
|
||||||
|
user_response["quotas"] = [quota_view(quota) for quota in quotas] if quotas else []
|
||||||
|
user_response["quota_report"] = model.namespacequota.get_quota_for_view(user.username)
|
||||||
|
|
||||||
user_view_perm = UserReadPermission(user.username)
|
user_view_perm = UserReadPermission(user.username)
|
||||||
if user_view_perm.can():
|
if user_view_perm.can():
|
||||||
user_response.update(
|
user_response.update(
|
||||||
@ -217,6 +222,24 @@ def notification_view(note):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def quota_view(quota):
|
||||||
|
quota_limits = list(model.namespacequota.get_namespace_quota_limit_list(quota))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": quota.id, # Generate uuid instead?
|
||||||
|
"limit_bytes": quota.limit_bytes,
|
||||||
|
"limits": [limit_view(limit) for limit in quota_limits],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def limit_view(limit):
|
||||||
|
return {
|
||||||
|
"id": limit.id,
|
||||||
|
"type": limit.quota_type.name,
|
||||||
|
"limit_percent": limit.percent_of_limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/user/")
|
@resource("/v1/user/")
|
||||||
class User(ApiResource):
|
class User(ApiResource):
|
||||||
"""
|
"""
|
||||||
|
@ -866,10 +866,12 @@ def populate_database(minimal=False):
|
|||||||
QuotaType.create(name="Warning")
|
QuotaType.create(name="Warning")
|
||||||
QuotaType.create(name="Reject")
|
QuotaType.create(name="Reject")
|
||||||
|
|
||||||
model.namespacequota.create_namespace_quota(org.username, 3050)
|
quota1 = model.namespacequota.create_namespace_quota(org, 3000)
|
||||||
|
model.namespacequota.create_namespace_quota_limit(quota1, "warning", 50)
|
||||||
model.repository.force_cache_repo_size(publicrepo.id)
|
model.repository.force_cache_repo_size(publicrepo.id)
|
||||||
|
|
||||||
model.namespacequota.create_namespace_limit(org.username, 1, 50)
|
quota2 = model.namespacequota.create_namespace_quota(new_user_4, 6000)
|
||||||
|
model.namespacequota.create_namespace_quota_limit(quota2, "reject", 90)
|
||||||
|
|
||||||
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
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
<span class="cor-options-menu"
|
<span class="cor-options-menu"
|
||||||
ng-if="user.username != current_user.username && !current_user.super_user && !inReadOnlyMode">
|
ng-if="user.username != current_user.username && !current_user.super_user && !inReadOnlyMode">
|
||||||
<span class="cor-option" option-click="showChangeEmail(current_user)"
|
<span class="cor-option" option-click="showChangeEmail(current_user)"
|
||||||
quay-show="Config.AUTHENTICATION_TYPE == 'Database' || Config>.AUTHENTICATION_TYPE == 'AppToken'">
|
quay-show="Config.AUTHENTICATION_TYPE == 'Database' || Config.AUTHENTICATION_TYPE == 'AppToken'">
|
||||||
<i class="fa fa-envelope-o"></i> Change E-mail Address
|
<i class="fa fa-envelope-o"></i> Change E-mail Address
|
||||||
</span>
|
</span>
|
||||||
<span class="cor-option" option-click="showChangePassword(current_user)"
|
<span class="cor-option" option-click="showChangePassword(current_user)"
|
||||||
|
@ -1,69 +1,102 @@
|
|||||||
<div class="quota-management-view-element">
|
<div class="quota-management-view-element">
|
||||||
<table class="co-list-table">
|
<form>
|
||||||
<tr>
|
<table class="co-list-table">
|
||||||
<td>
|
<tr>
|
||||||
Quota Management:
|
<td>
|
||||||
</td>
|
Quota Management:
|
||||||
<td>
|
</td>
|
||||||
<fieldset ng-disabled="disabled">
|
<td>
|
||||||
<div class="row-alignment">
|
<table class="co-table">
|
||||||
|
<td>
|
||||||
|
<input class="margin-2 form-control width-440" type="number" name="quota-limit" min="1" ng-model="currentQuotaConfig['quota']">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control"
|
||||||
|
ng-model="currentQuotaConfig['byte_unit']"
|
||||||
|
ng-options="val for val in quotaUnits">
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="margin-2 btn btn-primary save-quota-details margin-3"
|
||||||
|
ng-disabled="disableSaveQuota()" ng-click="updateQuotaConfig()">Save Quota Details</button>
|
||||||
|
</td>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Limits:
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<table class="co-table" ng-if="currentQuotaConfig">
|
||||||
|
|
||||||
<label class="margin-2 width-100">Set Quota: </label>
|
<thead>
|
||||||
|
<td class="hidden-xs">
|
||||||
<input class="margin-2 form-control width-440" type="number" name="quota-limit" ng-model="currentQuotaConfig['quota']">
|
<span>Action</span>
|
||||||
<select class="form-control"
|
</td>
|
||||||
ng-model="currentQuotaConfig['bytes_unit']"
|
<td class="hidden-xs">
|
||||||
ng-options="val for val in quotaUnits">
|
<span>Limit Percent</span>
|
||||||
</select>
|
</td>
|
||||||
|
<td class="hidden-xs">
|
||||||
<button class="margin-2 add-quota-limit btn btn-success hidden-xs" ng-click="addQuotaLimit($event)">
|
<span></span>
|
||||||
<i class="fa fa-plus margin-right-4"></i>
|
</td>
|
||||||
Add Quota Limit
|
</thead>
|
||||||
</button>
|
|
||||||
</div>
|
<!-- Update limits -->
|
||||||
|
<tr ng-repeat="limit in currentQuotaConfig['limits']">
|
||||||
<table class="co-table" ng-if="limitCounter > 0">
|
<td>
|
||||||
<thead>
|
<select class="form-control" name="quotaLimitType" id="quotaLimitType"
|
||||||
<td class="hidden-xs">
|
ng-options="type for type in quotaLimitTypes"
|
||||||
<span>Action</span>
|
ng-model="limit['type']">
|
||||||
</td>
|
</select>
|
||||||
<td class="hidden-xs">
|
</td>
|
||||||
<span>Limit Percent</span>
|
<td>
|
||||||
</td>
|
<input class="form-control margin-2" type="number" name="limit-percent"
|
||||||
<td class="hidden-xs">
|
min="1" max="100" placeholder="Limit Percent" ng-model="limit['limit_percent']"/>
|
||||||
<span></span>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
</thead>
|
<span class="margin-2">
|
||||||
|
<button class="margin-2 btn btn-primary" ng-click="updateQuotaLimit(limit.id)" ng-disabled="disableUpdateQuota(limit.id)">
|
||||||
<tbody ng-model="limitCounter" ng-repeat="x in [].constructor(limitCounter) track by $index" id="limit-id-limitCounter">
|
<i class="fa fa-trash"></i>
|
||||||
<tr>
|
Update
|
||||||
<td>
|
</button>
|
||||||
<select class="form-control" name="quotaLimitType" id="quotaLimitType"
|
</span>
|
||||||
ng-options="obj as obj.name for obj in quotaLimitTypes track by obj.name"
|
<span class="margin-2">
|
||||||
ng-model="currentQuotaConfig['limits'][$index]['limit_type']">
|
<button class="margin-2 btn btn-danger" ng-click="deleteQuotaLimit(limit.id)">
|
||||||
</select>
|
<i class="fa fa-trash"></i>
|
||||||
</td>
|
Remove
|
||||||
|
</button>
|
||||||
<td>
|
</span>
|
||||||
<input class="form-control margin-2" type="number" name="limit-percent" min="1" max="100" placeholder="Limit Percent" ng-model="currentQuotaConfig['limits'][$index]['percent_of_limit']"/>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
|
|
||||||
<td>
|
<!-- Add limit -->
|
||||||
<span class="margin-2" ng-hide="limitCounter < 1">
|
<tr>
|
||||||
<button class="margin-2 btn btn-danger" ng-click="removeQuotaLimit($index)" ng-model="$index">
|
<td>
|
||||||
<i class="fa fa-trash"></i>
|
<select class="form-control" name="quotaLimitType" id="quotaLimitType"
|
||||||
Remove
|
ng-options="type for type in quotaLimitTypes" ng-model="newLimitConfig['type']">
|
||||||
</button>
|
</select>
|
||||||
</span>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
</tr>
|
<input class="form-control margin-2" type="number" name="limit-percent"
|
||||||
</tbody>
|
min="1" max="100" placeholder="Percent threshold" ng-model="newLimitConfig['limit_percent']"/>
|
||||||
</table>
|
</td>
|
||||||
|
<td>
|
||||||
<button class="margin-2 btn btn-primary save-quota-details" ng-disabled="disableSave()" ng-click="updateQuotaDetailsOnSave()">Save Quota Details</button>
|
<span class="margin-2">
|
||||||
</fieldset>
|
<button class="add-quota-limit btn btn-success hidden-xs" ng-click="addQuotaLimit()" ng-disabled="!prevQuotaConfig['quota']">
|
||||||
|
<i class="fa fa-plus margin-right-4"></i>
|
||||||
</td>
|
Add Quota Limit
|
||||||
</tr>
|
</button>
|
||||||
</table>
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<button class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,10 +38,10 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="hidden-xs"
|
<td class="hidden-xs"
|
||||||
ng-class="tablePredicateClass('quota', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('quota_report', options.predicate, options.reverse)"
|
||||||
style="min-width: 20px;"
|
style="min-width: 20px;"
|
||||||
ng-if="quotaManagementEnabled">
|
ng-if="quotaManagementEnabled">
|
||||||
<a ng-click="orderBy('quota.quota_bytes')">Quota Consumed</a>
|
<a ng-click="orderBy('quota_report.quota_bytes')">Quota Consumed</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden-xs"
|
<td class="hidden-xs"
|
||||||
ng-class="tablePredicateClass('popularity', options.predicate, options.reverse)"
|
ng-class="tablePredicateClass('popularity', options.predicate, options.reverse)"
|
||||||
@ -81,12 +81,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="repo-quota hidden-xs" ng-show="quotaManagementEnabled">
|
<td class="repo-quota hidden-xs" ng-show="quotaManagementEnabled">
|
||||||
<span ng-if="::repository.quota.percent_consumed != null">
|
<span ng-if="::repository.quota_report.configured_quota != null">
|
||||||
<span ng-bind="::bytesToHumanReadableString(repository.quota.quota_bytes)"></span> ({{::repository.quota.percent_consumed}}%)
|
<span ng-bind="::bytesToHumanReadableString(repository.quota_report.quota_bytes)"></span>
|
||||||
|
{{ ::quotaPercentConsumed(repository) }}%
|
||||||
|
</span>
|
||||||
|
<span ng-if="::repository.quota_report.configured_quota == null">
|
||||||
|
--
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="::repository.quota.percent_consumed == null">
|
|
||||||
<span ng-bind="::bytesToHumanReadableString(repository.quota.quota_bytes)"></span>
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="popularity hidden-xs">
|
<td class="popularity hidden-xs">
|
||||||
<span class="strength-indicator" value="::repository.popularity" maximum="::maxPopularity"
|
<span class="strength-indicator" value="::repository.popularity" maximum="::maxPopularity"
|
||||||
|
@ -2,289 +2,213 @@
|
|||||||
* An element which displays a panel for managing users.
|
* An element which displays a panel for managing users.
|
||||||
*/
|
*/
|
||||||
angular.module('quay').directive('quotaManagementView', function () {
|
angular.module('quay').directive('quotaManagementView', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
templateUrl: '/static/directives/quota-management-view.html',
|
templateUrl: '/static/directives/quota-management-view.html',
|
||||||
restrict: 'AEC',
|
restrict: 'AEC',
|
||||||
scope: {
|
scope: {
|
||||||
'organization': '=organization',
|
'isEnabled': '=isEnabled',
|
||||||
'disabled': '=disabled'
|
'organization': '=organization',
|
||||||
},
|
},
|
||||||
controller: function ($scope, $timeout, $location, $element, ApiService, UserService,
|
controller: function ($scope, $timeout, $location, $element, ApiService, UserService,
|
||||||
TableService, Features, StateService, $q) {
|
TableService, Features, StateService, $q) {
|
||||||
$scope.prevquotaEnabled = false;
|
$scope.prevquotaEnabled = false;
|
||||||
$scope.updating = false;
|
$scope.updating = false;
|
||||||
$scope.limitCounter = 0;
|
$scope.limitCounter = 0;
|
||||||
$scope.quotaLimitTypes = [];
|
$scope.quotaLimitTypes = [
|
||||||
$scope.prevQuotaConfig = {'limit_bytes': null, 'quota': null, 'limits': [], 'bytes_unit': null};
|
"Reject", "Warning"
|
||||||
$scope.currentQuotaConfig = {'limit_bytes': null, 'quota': null, 'limits': [], 'bytes_unit': null};
|
];
|
||||||
$scope.defer = null;
|
|
||||||
$scope.disk_size_units = {
|
|
||||||
'MB': 1024**2,
|
|
||||||
'GB': 1024**3,
|
|
||||||
'TB': 1024**4,
|
|
||||||
};
|
|
||||||
$scope.quotaUnits = Object.keys($scope.disk_size_units);
|
|
||||||
$scope.rejectLimitType = 'Reject';
|
|
||||||
$scope.isUpdateable = false;
|
|
||||||
|
|
||||||
var loadOrgQuota = function (fresh) {
|
$scope.prevQuotaConfig = {'quota': null, 'limits': {}};
|
||||||
$scope.nameSpaceResource = ApiService.getNamespaceQuota(null,
|
$scope.currentQuotaConfig = {'quota': null, 'limits': {}};
|
||||||
{'namespace': $scope.organization.name}).then((resp) => {
|
$scope.newLimitConfig = {'type': null, 'limit_percent': null}
|
||||||
$scope.prevQuotaConfig['limit_bytes'] = $scope.currentQuotaConfig['limit_bytes'] = resp["limit_bytes"];
|
|
||||||
let { result, byte_unit } = bytes_to_human_readable_string(resp["limit_bytes"]);
|
|
||||||
$scope.prevQuotaConfig['quota'] = $scope.currentQuotaConfig['quota'] = result
|
|
||||||
$scope.prevQuotaConfig['bytes_unit'] = $scope.currentQuotaConfig['bytes_unit'] = byte_unit;
|
|
||||||
|
|
||||||
if (fresh) {
|
$scope.defer = null;
|
||||||
for (let i = 0; i < resp["quota_limit_types"].length; i++) {
|
$scope.disk_size_units = {
|
||||||
let temp = resp["quota_limit_types"][i];
|
'KB': 1024,
|
||||||
temp["quota_limit_id"] = null;
|
'MB': 1024**2,
|
||||||
$scope.quotaLimitTypes.push(temp);
|
'GB': 1024**3,
|
||||||
}
|
'TB': 1024**4,
|
||||||
}
|
};
|
||||||
|
$scope.quotaUnits = Object.keys($scope.disk_size_units);
|
||||||
|
$scope.rejectLimitType = 'Reject';
|
||||||
|
|
||||||
if (resp["limit_bytes"] != null) {
|
var loadOrgQuota = function () {
|
||||||
$scope.prevquotaEnabled = true;
|
$scope.nameSpaceResource = ApiService.listOrganizationQuota(
|
||||||
}
|
null, {'orgname': $scope.organization.name}
|
||||||
});
|
).then((resp) => {
|
||||||
|
if (resp.length > 0) {
|
||||||
|
quota = resp[0];
|
||||||
|
$scope.prevQuotaConfig['id'] = quota["id"];
|
||||||
|
$scope.currentQuotaConfig['id'] = quota["id"];
|
||||||
|
|
||||||
|
for (i in quota['limits']) {
|
||||||
|
limitId = quota['limits'][i]['id'];
|
||||||
|
$scope.prevQuotaConfig['limits'][limitId] = $.extend({}, quota["limits"][i]);
|
||||||
|
$scope.currentQuotaConfig['limits'][limitId] = $.extend({}, quota["limits"][i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let { result, byte_unit } = normalizeLimitBytes(quota["limit_bytes"]);
|
||||||
|
$scope.prevQuotaConfig['quota'] = result;
|
||||||
|
$scope.currentQuotaConfig['quota'] = result;
|
||||||
|
$scope.prevQuotaConfig['byte_unit'] = byte_unit;
|
||||||
|
$scope.currentQuotaConfig['byte_unit'] = byte_unit;
|
||||||
|
|
||||||
|
if (quota["limit_bytes"] != null) {
|
||||||
|
$scope.prevquotaEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.organization.quota_report.configured_quota = quota["limit_bytes"];
|
||||||
|
$scope.organization.quota_report.percent_consumed = (parseInt($scope.organization.quota_report.quota_bytes) / $scope.organization.quota_report.configured_quota * 100).toFixed(2);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var human_readable_string_to_bytes = function(quota, bytes_unit) {
|
var humanReadableStringToBytes = function(quota, bytes_unit) {
|
||||||
return Number(quota*$scope.disk_size_units[bytes_unit]);
|
return Number(quota*$scope.disk_size_units[bytes_unit]);
|
||||||
};
|
};
|
||||||
|
|
||||||
var bytes_to_human_readable_string = function (bytes) {
|
var normalizeLimitBytes = function (bytes) {
|
||||||
let units = Object.keys($scope.disk_size_units).reverse();
|
let units = Object.keys($scope.disk_size_units).reverse();
|
||||||
let result = null;
|
let result = null;
|
||||||
let byte_unit = null;
|
let byte_unit = null;
|
||||||
for (const key in units) {
|
|
||||||
byte_unit = units[key];
|
|
||||||
if (bytes >= $scope.disk_size_units[byte_unit]) {
|
|
||||||
result = bytes / $scope.disk_size_units[byte_unit];
|
|
||||||
return { result, byte_unit };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { result, byte_unit };
|
|
||||||
};
|
|
||||||
|
|
||||||
var loadQuotaLimits = function (fresh) {
|
for (const key in units) {
|
||||||
$scope.nameSpaceQuotaLimitsResource = ApiService.getOrganizationQuotaLimit(null,
|
byte_unit = units[key];
|
||||||
{'namespace': $scope.organization.name}).then((resp) => {
|
result = bytes / $scope.disk_size_units[byte_unit];
|
||||||
$scope.prevQuotaConfig['limits'] = [];
|
if (bytes >= $scope.disk_size_units[byte_unit]) {
|
||||||
$scope.currentQuotaConfig['limits'] = [];
|
return { result, byte_unit };
|
||||||
for (let i = 0; i < resp['quota_limits'].length; i ++) {
|
}
|
||||||
$scope.prevQuotaConfig['limits'].push({...resp['quota_limits'][i]});
|
|
||||||
$scope.currentQuotaConfig['limits'].push({...resp['quota_limits'][i]});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fresh) {
|
|
||||||
if ($scope.currentQuotaConfig['limits']) {
|
|
||||||
for (let i = 0; i < $scope.currentQuotaConfig['limits'].length; i++) {
|
|
||||||
populateQuotaLimit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateOrganizationQuota = function(params) {
|
|
||||||
|
|
||||||
if (!$scope.prevquotaEnabled || $scope.prevQuotaConfig['quota'] != $scope.currentQuotaConfig['quota']
|
|
||||||
|| $scope.prevQuotaConfig['bytes_unit'] != $scope.currentQuotaConfig['bytes_unit'] ) {
|
|
||||||
let quotaMethod = ApiService.createNamespaceQuota;
|
|
||||||
let m1 = "createNamespaceQuota";
|
|
||||||
let limit_bytes = human_readable_string_to_bytes($scope.currentQuotaConfig['quota'], $scope.currentQuotaConfig['bytes_unit']);
|
|
||||||
let data = {
|
|
||||||
'limit_bytes': limit_bytes,
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($scope.prevquotaEnabled) {
|
|
||||||
quotaMethod = ApiService.changeOrganizationQuota;
|
|
||||||
m1 = "changeOrganizationQuota";
|
|
||||||
}
|
|
||||||
|
|
||||||
quotaMethod(data, params).then((resp) => {
|
|
||||||
$scope.updating = false;
|
|
||||||
loadOrgQuota(false);
|
|
||||||
}, displayError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var createOrgQuotaLimit = function(data, params) {
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
let to_send = {
|
|
||||||
'percent_of_limit': data[i]['percent_of_limit'],
|
|
||||||
'quota_type_id': data[i]['limit_type']['quota_type_id']
|
|
||||||
};
|
|
||||||
ApiService.createOrganizationQuotaLimit(to_send, params).then((resp) => {
|
|
||||||
$scope.prevquotaEnabled = true;
|
|
||||||
}, displayError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateOrgQuotaLimit = function(data, params) {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
let to_send = {
|
|
||||||
'percent_of_limit': data[i]['percent_of_limit'],
|
|
||||||
'quota_type_id': data[i]['limit_type']['quota_type_id'],
|
|
||||||
'quota_limit_id': data[i]['limit_type']['quota_limit_id']
|
|
||||||
};
|
|
||||||
ApiService.changeOrganizationQuotaLimit(to_send, params).then((resp) => {
|
|
||||||
$scope.prevquotaEnabled = true;
|
|
||||||
}, displayError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var deleteOrgQuotaLimit = function(data, params) {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
params['quota_limit_id'] = data[i]['limit_type']['quota_limit_id'];
|
|
||||||
ApiService.deleteOrganizationQuotaLimit(null, params).then((resp) => {
|
|
||||||
$scope.prevquotaEnabled = true;
|
|
||||||
}, displayError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var similarLimits =function() {
|
|
||||||
return JSON.stringify($scope.prevQuotaConfig['limits']) === JSON.stringify($scope.currentQuotaConfig['limits']);
|
|
||||||
}
|
|
||||||
|
|
||||||
var fetchLimitsToDelete = function() {
|
|
||||||
// In prev but not in current => to be deleted
|
|
||||||
let currentQuotaConfig = $scope.currentQuotaConfig['limits'];
|
|
||||||
let prevQuotaConfig = $scope.prevQuotaConfig['limits'];
|
|
||||||
return prevQuotaConfig.filter(function(obj1) {
|
|
||||||
return obj1.limit_type.quota_limit_id != null && !currentQuotaConfig.some(function(obj2) {
|
|
||||||
return obj1.limit_type.quota_limit_id === obj2.limit_type.quota_limit_id;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var fetchLimitsToAdd = function() {
|
|
||||||
// In current but not in prev => to add
|
|
||||||
let currentQuotaConfig = $scope.currentQuotaConfig['limits'];
|
|
||||||
let prevQuotaConfig = $scope.prevQuotaConfig['limits'];
|
|
||||||
return currentQuotaConfig.filter(function(obj1) {
|
|
||||||
return obj1.limit_type.quota_limit_id == null && !prevQuotaConfig.some(function(obj2) {
|
|
||||||
return obj1.limit_type.name === obj2.limit_type.name && obj1.percent_of_limit === obj2.percent_of_limit;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var fetchLimitsToUpdate = function() {
|
|
||||||
// In current and prev but different values
|
|
||||||
let currentQuotaConfig = $scope.currentQuotaConfig['limits'];
|
|
||||||
let prevQuotaConfig = $scope.prevQuotaConfig['limits'];
|
|
||||||
return currentQuotaConfig.filter(function(obj1) {
|
|
||||||
return prevQuotaConfig.some(function(obj2) {
|
|
||||||
return obj1.limit_type.quota_limit_id == obj2.limit_type.quota_limit_id &&
|
|
||||||
(obj1.percent_of_limit != obj2.percent_of_limit || obj1.limit_type.name != obj2.limit_type.name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateQuotaLimits = function(params) {
|
|
||||||
if (similarLimits()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let toDelete = fetchLimitsToDelete();
|
|
||||||
let toAdd = fetchLimitsToAdd();
|
|
||||||
let toUpdate = fetchLimitsToUpdate();
|
|
||||||
|
|
||||||
createOrgQuotaLimit(toAdd, params);
|
|
||||||
updateOrgQuotaLimit(toUpdate, params);
|
|
||||||
deleteOrgQuotaLimit(toDelete, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayError = function(message = 'Could not update quota details') {
|
|
||||||
$scope.updating = true;
|
|
||||||
let errorDisplay = ApiService.errorDisplay(message, () => {
|
|
||||||
$scope.updating = false;
|
|
||||||
});
|
|
||||||
return errorDisplay;
|
|
||||||
}
|
|
||||||
|
|
||||||
var validLimits = function() {
|
|
||||||
let valid = true;
|
|
||||||
let rejectCount = 0;
|
|
||||||
for (let i = 0; i < $scope.currentQuotaConfig['limits'].length; i++) {
|
|
||||||
if ($scope.currentQuotaConfig['limits'][i]['limit_type']['name'] === $scope.rejectLimitType) {
|
|
||||||
rejectCount++;
|
|
||||||
|
|
||||||
if (rejectCount > 1) {
|
|
||||||
let alert = displayError('You can only have one Reject type of Quota Limits. Please remove to proceed');
|
|
||||||
alert();
|
|
||||||
valid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.disableSave = function() {
|
|
||||||
return $scope.prevQuotaConfig['quota'] === $scope.currentQuotaConfig['quota'] &&
|
|
||||||
$scope.prevQuotaConfig['bytes_unit'] === $scope.currentQuotaConfig['bytes_unit'] &&
|
|
||||||
similarLimits();
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateQuotaDetails = function() {
|
|
||||||
// Validate correctness
|
|
||||||
if (!validLimits()) {
|
|
||||||
$scope.defer.resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let params = {
|
|
||||||
'namespace': $scope.organization.name
|
|
||||||
};
|
|
||||||
|
|
||||||
updateOrganizationQuota(params);
|
|
||||||
updateQuotaLimits(params);
|
|
||||||
$scope.defer.resolve();
|
|
||||||
$scope.isUpdateable = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.updateQuotaDetailsOnSave = function() {
|
|
||||||
$scope.defer = $q.defer();
|
|
||||||
updateQuotaDetails();
|
|
||||||
if ($scope.isUpdateable) {
|
|
||||||
$scope.defer.promise.then(function() {
|
|
||||||
loadOrgQuota(false);
|
|
||||||
loadQuotaLimits(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$scope.isUpdateable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.addQuotaLimit = function($event) {
|
|
||||||
$scope.limitCounter++;
|
|
||||||
let temp = {'percent_of_limit': '', 'limit_type': $scope.quotaLimitTypes[0]};
|
|
||||||
$scope.currentQuotaConfig['limits'].push(temp);
|
|
||||||
$event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
var populateQuotaLimit = function() {
|
|
||||||
$scope.limitCounter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.removeQuotaLimit = function(index) {
|
|
||||||
$scope.currentQuotaConfig['limits'].splice(index, 1);
|
|
||||||
$scope.limitCounter--;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadOrgQuota(true);
|
|
||||||
loadQuotaLimits(true);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return { result, byte_unit };
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateOrganizationQuota = function(params) {
|
||||||
|
let limit_bytes = humanReadableStringToBytes($scope.currentQuotaConfig['quota'], $scope.currentQuotaConfig['byte_unit']);
|
||||||
|
let data = {'limit_bytes': limit_bytes};
|
||||||
|
let quotaMethod = null;
|
||||||
|
|
||||||
return directiveDefinitionObject;
|
if (!$scope.prevquotaEnabled ||
|
||||||
|
$scope.prevQuotaConfig['quota'] != $scope.currentQuotaConfig['quota'] ||
|
||||||
|
$scope.prevQuotaConfig['byte_unit'] != $scope.currentQuotaConfig['byte_unit']) {
|
||||||
|
if ($scope.prevquotaEnabled) {
|
||||||
|
quotaMethod = ApiService.changeOrganizationQuota;
|
||||||
|
} else {
|
||||||
|
quotaMethod = ApiService.createOrganizationQuota;
|
||||||
|
}
|
||||||
|
quotaMethod(data, params).then((resp) => {
|
||||||
|
loadOrgQuota();
|
||||||
|
}, displayError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayError = function(message = 'Could not update quota details') {
|
||||||
|
$scope.updating = true;
|
||||||
|
let errorDisplay = ApiService.errorDisplay(message, () => {
|
||||||
|
$scope.updating = false;
|
||||||
|
});
|
||||||
|
return errorDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validLimits = function() {
|
||||||
|
let valid = true;
|
||||||
|
let rejectCount = 0;
|
||||||
|
for (let i = 0; i < $scope.currentQuotaConfig['limits'].length; i++) {
|
||||||
|
if ($scope.currentQuotaConfig['limits'][i]['type'] === $scope.rejectLimitType) {
|
||||||
|
rejectCount++;
|
||||||
|
|
||||||
|
if (rejectCount > 1) {
|
||||||
|
let alert = displayError('You can only have one Reject type of Quota Limits. Please remove to proceed');
|
||||||
|
alert();
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.updateQuotaConfig = function() {
|
||||||
|
// Validate correctness
|
||||||
|
if (!validLimits()) {
|
||||||
|
$scope.defer.resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
'orgname': $scope.organization.name,
|
||||||
|
'quota_id': $scope.currentQuotaConfig.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateOrganizationQuota(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.addQuotaLimit = function() {
|
||||||
|
var params = {
|
||||||
|
'orgname': $scope.organization.name,
|
||||||
|
'quota_id': $scope.currentQuotaConfig.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'type': $scope.newLimitConfig['type'],
|
||||||
|
'threshold_percent': $scope.newLimitConfig['limit_percent'],
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.createOrganizationQuotaLimit(data, params).then((resp) => {
|
||||||
|
$scope.newLimitConfig['type'] = null;
|
||||||
|
$scope.newLimitConfig['limit_percent'] = null;
|
||||||
|
loadOrgQuota();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.updateQuotaLimit = function(limitId) {
|
||||||
|
var params = {
|
||||||
|
'orgname': $scope.organization.name,
|
||||||
|
'quota_id': $scope.currentQuotaConfig.id,
|
||||||
|
'limit_id': limitId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'type': $scope.currentQuotaConfig['limits'][limitId]['type'],
|
||||||
|
'threshold_percent': $scope.currentQuotaConfig['limits'][limitId]['limit_percent'],
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.changeOrganizationQuotaLimit(data, params).then((resp) => {
|
||||||
|
$scope.prevQuotaConfig['limits'][limitId]['type'] = $scope.currentQuotaConfig['limits'][limitId]['type'];
|
||||||
|
$scope.prevQuotaConfig['limits'][limitId]['limit_percent'] = $scope.currentQuotaConfig['limits'][limitId]['limit_percent'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.deleteQuotaLimit = function(limitId) {
|
||||||
|
params = {
|
||||||
|
'orgname': $scope.organization.name,
|
||||||
|
'quota_id': $scope.currentQuotaConfig.id,
|
||||||
|
'limit_id': limitId,
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiService.deleteOrganizationQuotaLimit(null, params).then((resp) => {
|
||||||
|
delete $scope.currentQuotaConfig['limits'][limitId];
|
||||||
|
delete $scope.prevQuotaConfig['limits'][limitId];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.disableSaveQuota = function() {
|
||||||
|
return $scope.prevQuotaConfig['quota'] === $scope.currentQuotaConfig['quota'] &&
|
||||||
|
$scope.prevQuotaConfig['byte_unit'] === $scope.currentQuotaConfig['byte_unit'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.disableUpdateQuota = function(limitId) {
|
||||||
|
return $scope.prevQuotaConfig['limits'][limitId]['type'] === $scope.currentQuotaConfig['limits'][limitId]['type'] &&
|
||||||
|
$scope.prevQuotaConfig['limits'][limitId]['limit_percent'] === $scope.currentQuotaConfig['limits'][limitId]['limit_percent'];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOrgQuota();
|
||||||
|
/* loadQuotaLimits(true); */
|
||||||
|
$scope.$watch('isEnabled', loadOrgQuota);
|
||||||
|
$scope.$watch('organization', loadOrgQuota);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return directiveDefinitionObject;
|
||||||
});
|
});
|
||||||
|
@ -30,6 +30,7 @@ angular.module('quay').directive('repoListTable', function () {
|
|||||||
'page': 0
|
'page': 0
|
||||||
};
|
};
|
||||||
$scope.disk_size_units = {
|
$scope.disk_size_units = {
|
||||||
|
'KB': 1024,
|
||||||
'MB': 1024**2,
|
'MB': 1024**2,
|
||||||
'GB': 1024**3,
|
'GB': 1024**3,
|
||||||
'TB': 1024**4,
|
'TB': 1024**4,
|
||||||
@ -41,7 +42,7 @@ angular.module('quay').directive('repoListTable', function () {
|
|||||||
|
|
||||||
$scope.orderedRepositories = TableService.buildOrderedItems($scope.repositories,
|
$scope.orderedRepositories = TableService.buildOrderedItems($scope.repositories,
|
||||||
$scope.options,
|
$scope.options,
|
||||||
['namespace', 'name', 'state'], ['last_modified_datetime', 'popularity', 'quota'])
|
['namespace', 'name', 'state'], ['last_modified_datetime', 'popularity', 'quota_report'])
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.tablePredicateClass = function(name, predicate, reverse) {
|
$scope.tablePredicateClass = function(name, predicate, reverse) {
|
||||||
@ -67,13 +68,21 @@ angular.module('quay').directive('repoListTable', function () {
|
|||||||
let result = null;
|
let result = null;
|
||||||
let byte_unit = null;
|
let byte_unit = null;
|
||||||
for (const key in units) {
|
for (const key in units) {
|
||||||
byte_unit = units[key];
|
byte_unit = units[key];
|
||||||
if (bytes >= $scope.disk_size_units[byte_unit]) {
|
result = (bytes / $scope.disk_size_units[byte_unit]).toFixed(2);
|
||||||
result = (bytes / $scope.disk_size_units[byte_unit]).toFixed(2);
|
if (bytes >= $scope.disk_size_units[byte_unit]) {
|
||||||
return result.toString() + " " + byte_unit;
|
return result.toString() + " " + byte_unit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
|
return result.toString() + " " + byte_unit;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.quotaPercentConsumed = function(repository) {
|
||||||
|
if (repository.quota_report) {
|
||||||
|
return (repository.quota_report.quota_bytes / repository.quota_report.configured_quota * 100).toFixed(2);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getAvatarData = function(namespace) {
|
$scope.getAvatarData = function(namespace) {
|
||||||
|
@ -33,8 +33,7 @@
|
|||||||
'organizationEmail': ''
|
'organizationEmail': ''
|
||||||
};
|
};
|
||||||
$scope.disk_size_units = {
|
$scope.disk_size_units = {
|
||||||
'Bytes': 1,
|
'KB': 1024,
|
||||||
'KB': 1024**1,
|
|
||||||
'MB': 1024**2,
|
'MB': 1024**2,
|
||||||
'GB': 1024**3,
|
'GB': 1024**3,
|
||||||
'TB': 1024**4,
|
'TB': 1024**4,
|
||||||
@ -47,17 +46,26 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$scope.bytesToHumanReadableString = function(bytes) {
|
$scope.bytesToHumanReadableString = function(bytes) {
|
||||||
let units = Object.keys($scope.disk_size_units).reverse();
|
let units = Object.keys($scope.disk_size_units).reverse();
|
||||||
let result = null;
|
let result = null;
|
||||||
let byte_unit = null;
|
let byte_unit = null;
|
||||||
for (const key in units) {
|
|
||||||
byte_unit = units[key];
|
for (const key in units) {
|
||||||
if (bytes >= $scope.disk_size_units[byte_unit]) {
|
byte_unit = units[key];
|
||||||
result = (bytes / $scope.disk_size_units[byte_unit]).toFixed(2);
|
result = (bytes / $scope.disk_size_units[byte_unit]).toFixed(2);
|
||||||
return result.toString() + " " + byte_unit;
|
if (bytes >= $scope.disk_size_units[byte_unit]) {
|
||||||
}
|
return result.toString() + " " + byte_unit;
|
||||||
}
|
}
|
||||||
return null
|
}
|
||||||
|
|
||||||
|
return result.toString() + " " + byte_unit;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.quotaPercentConsumed = function(organization) {
|
||||||
|
if (organization.quota_report) {
|
||||||
|
return (organization.quota_report.quota_bytes / organization.quota_report.configured_quota * 100).toFixed(2);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
var loadRepositories = function() {
|
var loadRepositories = function() {
|
||||||
|
@ -42,11 +42,12 @@
|
|||||||
'page': 0,
|
'page': 0,
|
||||||
}
|
}
|
||||||
$scope.disk_size_units = {
|
$scope.disk_size_units = {
|
||||||
'MB': 1024**2,
|
'KB': 1024,
|
||||||
'GB': 1024**3,
|
'MB': 1024**2,
|
||||||
'TB': 1024**4,
|
'GB': 1024**3,
|
||||||
};
|
'TB': 1024**4,
|
||||||
$scope.quotaUnits = Object.keys($scope.disk_size_units);
|
};
|
||||||
|
$scope.quotaUnits = Object.keys($scope.disk_size_units);
|
||||||
|
|
||||||
$scope.showQuotaConfig = function (org) {
|
$scope.showQuotaConfig = function (org) {
|
||||||
if (StateService.inReadOnlyMode()) {
|
if (StateService.inReadOnlyMode()) {
|
||||||
@ -67,8 +68,8 @@
|
|||||||
return result.toString() + " " + byte_unit;
|
return result.toString() + " " + byte_unit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return (bytes / $scope.disk_size_units["MB"]).toFixed(2).toString() + " MB";
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loadMessageOfTheDay = function () {
|
$scope.loadMessageOfTheDay = function () {
|
||||||
$scope.globalMessagesActive = true;
|
$scope.globalMessagesActive = true;
|
||||||
|
@ -58,18 +58,15 @@
|
|||||||
<div class="repo-list-view" namespaces="[organization]" in-read-only-mode="inReadOnlyMode"
|
<div class="repo-list-view" namespaces="[organization]" in-read-only-mode="inReadOnlyMode"
|
||||||
quota-management-enabled="quotaManagementEnabled">
|
quota-management-enabled="quotaManagementEnabled">
|
||||||
<h3>Repositories</h3>
|
<h3>Repositories</h3>
|
||||||
<span ng-show="Features.QUOTA_MANAGEMENT && organization.quota.configured_quota">
|
<span ng-show="Features.QUOTA_MANAGEMENT && organization.quotas.length > 0">
|
||||||
<h4>
|
<h4>
|
||||||
Total Quota Consumed:
|
Total Quota Consumed:
|
||||||
<span ng-if="organization.quota.quota_bytes != '0'">
|
<span ng-if="organization.quota_report.quota_bytes">
|
||||||
{{ organization.quota.percent_consumed ?
|
{{ bytesToHumanReadableString(organization.quota_report.quota_bytes) + "(" + quotaPercentConsumed(organization) + "%)" }}
|
||||||
(bytesToHumanReadableString(organization.quota.quota_bytes) + "(" + organization.quota.percent_consumed + "%)") :
|
</span>
|
||||||
bytesToHumanReadableString(organization.quota.quota_bytes) }}
|
<span ng-if="organization.quota_report.quota_bytes == '0'">--</span>
|
||||||
</span>
|
</h4>
|
||||||
<span ng-if="organization.quota.quota_bytes == '0'">--</span>
|
</span>
|
||||||
{{ " of " + bytesToHumanReadableString(organization.quota.configured_quota) }}
|
|
||||||
</h4>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</cor-tab-pane>
|
</cor-tab-pane>
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td ng-class="tablePredicateClass('quota', options.predicate, options.reverse)"
|
<td ng-class="tablePredicateClass('quota', options.predicate, options.reverse)"
|
||||||
ng-if="Features.QUOTA_MANAGEMENT">
|
ng-if="Features.QUOTA_MANAGEMENT">
|
||||||
<a ng-click="orderBy('quota.quota_bytes')">Quota Consumed</a>
|
<a ng-click="orderBy('quota_report.quota_bytes')">Quota Consumed</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 24px;"></td>
|
<td style="width: 24px;"></td>
|
||||||
</thead>
|
</thead>
|
||||||
@ -112,16 +112,16 @@
|
|||||||
<a href="mailto:{{ current_org.email }}">{{ current_org.email }}</a>
|
<a href="mailto:{{ current_org.email }}">{{ current_org.email }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="Features.QUOTA_MANAGEMENT">
|
<td ng-if="Features.QUOTA_MANAGEMENT">
|
||||||
<span ng-if="current_org.quota.quota_bytes != '0'">
|
<span ng-if="current_org.quota_report.quota_bytes != '0'">
|
||||||
{{
|
{{
|
||||||
current_org.quota.percent_consumed ?
|
current_org.quota_report.percent_consumed ?
|
||||||
( bytesToHumanReadableString(current_org.quota.quota_bytes) + "(" + current_org.quota.percent_consumed + "%)") :
|
( bytesToHumanReadableString(current_org.quota_report.quota_bytes) + "(" + current_org.quota_report.percent_consumed + "%)") :
|
||||||
bytesToHumanReadableString(current_org.quota.quota_bytes)
|
bytesToHumanReadableString(current_org.quota_report.quota_bytes)
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="current_org.quota.quota_bytes == '0'">--</span>
|
<span ng-if="current_org.quota_report.quota_bytes == '0'">--</span>
|
||||||
<span ng-if="current_org.quota.configured_quota">
|
<span ng-if="current_org.quota_report.configured_quota">
|
||||||
{{ " of " + bytesToHumanReadableString(current_org.quota.configured_quota) }}
|
{{ " of " + bytesToHumanReadableString(current_org.quota_report.configured_quota) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">
|
<td style="text-align: center;">
|
||||||
|
@ -2107,9 +2107,11 @@ class TestListRepos(ApiTestCase):
|
|||||||
|
|
||||||
def test_listrepos_asguest(self):
|
def test_listrepos_asguest(self):
|
||||||
# Queries: Base + the list query
|
# Queries: Base + the list query
|
||||||
with assert_query_count(BASE_QUERY_COUNT + 1):
|
# TODO: Add quota queries
|
||||||
json = self.getJsonResponse(RepositoryList, params=dict(public=True))
|
with patch("features.QUOTA_MANAGEMENT", False):
|
||||||
self.assertEqual(len(json["repositories"]), 1)
|
with assert_query_count(BASE_QUERY_COUNT + 1):
|
||||||
|
json = self.getJsonResponse(RepositoryList, params=dict(public=True))
|
||||||
|
self.assertEqual(len(json["repositories"]), 1)
|
||||||
|
|
||||||
def assertPublicRepos(self, has_extras=False):
|
def assertPublicRepos(self, has_extras=False):
|
||||||
public_user = model.user.get_user("public")
|
public_user = model.user.get_user("public")
|
||||||
@ -2173,13 +2175,15 @@ class TestListRepos(ApiTestCase):
|
|||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
# Queries: Base + the list query + the popularity and last modified queries + full perms load
|
# Queries: Base + the list query + the popularity and last modified queries + full perms load
|
||||||
with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 5):
|
# TODO: Add quota queries
|
||||||
json = self.getJsonResponse(
|
with patch("features.QUOTA_MANAGEMENT", False):
|
||||||
RepositoryList,
|
with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 5):
|
||||||
params=dict(
|
json = self.getJsonResponse(
|
||||||
namespace=ORGANIZATION, public=False, last_modified=True, popularity=True
|
RepositoryList,
|
||||||
),
|
params=dict(
|
||||||
)
|
namespace=ORGANIZATION, public=False, last_modified=True, popularity=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
self.assertGreater(len(json["repositories"]), 0)
|
self.assertGreater(len(json["repositories"]), 0)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user