1
0
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:
Kenny Lee Sin Cheong
2022-04-07 14:11:55 -04:00
committed by GitHub
parent a79f7b6f40
commit 896a3aab3a
27 changed files with 1339 additions and 1004 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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,
} }

View File

@ -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

View File

@ -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():

View File

@ -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": [],

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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":

View File

@ -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()

View File

@ -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):

View File

@ -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()
] ]

View File

@ -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,
),
] ]

View File

@ -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):
""" """

View File

@ -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

View File

@ -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)"

View File

@ -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>

View File

@ -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"

View File

@ -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;
}); });

View File

@ -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) {

View File

@ -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() {

View File

@ -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;

View File

@ -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>

View File

@ -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;">

View File

@ -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)