-
Notifications
You must be signed in to change notification settings - Fork 154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implement CRUD API for managing Harbor per-project Quota #3090
base: topic/11-06-feat_implement_per-project_images_api_based_on_rbac
Are you sure you want to change the base?
Changes from all commits
8d1297c
9128ef7
c60bd90
af7596d
ed1a4b1
a1225c2
c8fece0
4f3448a
780a250
1a4bf70
63d5dbb
473b125
1e8ac1a
cfd21fc
a49f1ff
16b45d1
3128b0a
97a3022
1a03789
c118c7e
96fc374
a1f3c08
217a359
89ab7a7
896c44d
835d0c6
0a442c9
049dfe0
cae3fee
3d1c9cf
cb68069
1ec6c50
7ddcad7
90eaff7
79d85e2
15d396f
cd0d828
515d39d
b40f6e3
1c04e5a
fc3060c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Implement CRUD API for managing Harbor per-project Quota. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
from ai.backend.client.output.fields import group_fields | ||
from ai.backend.client.output.types import FieldSpec | ||
from ai.backend.common.utils import b64encode | ||
|
||
from ...cli.types import Undefined, undefined | ||
from ..session import api_session | ||
|
@@ -311,3 +312,101 @@ async def remove_users( | |
} | ||
data = await api_session.get().Admin._query(query, variables) | ||
return data["modify_group"] | ||
|
||
@api_function | ||
@classmethod | ||
async def get_container_registry_quota(cls, group_id: str) -> int: | ||
""" | ||
Get Quota Limit for the group's container registry. | ||
Currently only HarborV2 registry is supported. | ||
|
||
You need an admin privilege for this operation. | ||
""" | ||
query = textwrap.dedent( | ||
"""\ | ||
query($id: String!) { | ||
group_node(id: $id) { | ||
registry_quota | ||
} | ||
} | ||
""" | ||
) | ||
|
||
variables = {"id": b64encode(f"group_node:{group_id}")} | ||
data = await api_session.get().Admin._query(query, variables) | ||
return data["group_node"]["registry_quota"] | ||
|
||
@api_function | ||
@classmethod | ||
async def create_container_registry_quota(cls, group_id: str, quota: int) -> dict: | ||
""" | ||
Create Quota Limit for the group's container registry. | ||
Currently only HarborV2 registry is supported. | ||
|
||
You need an admin privilege for this operation. | ||
""" | ||
query = textwrap.dedent( | ||
"""\ | ||
mutation($scope_id: ScopeField!, $quota: Int!) { | ||
create_container_registry_quota( | ||
scope_id: $scope_id, quota: $quota) { | ||
ok msg | ||
} | ||
} | ||
""" | ||
) | ||
|
||
scope_id = f"project:{group_id}" | ||
variables = {"scope_id": scope_id, "quota": quota} | ||
data = await api_session.get().Admin._query(query, variables) | ||
return data["create_container_registry_quota"] | ||
|
||
@api_function | ||
@classmethod | ||
async def update_container_registry_quota(cls, group_id: str, quota: int) -> dict: | ||
""" | ||
Update Quota Limit for the group's container registry. | ||
Currently only HarborV2 registry is supported. | ||
|
||
You need an admin privilege for this operation. | ||
""" | ||
query = textwrap.dedent( | ||
"""\ | ||
mutation($scope_id: ScopeField!, $quota: Int!) { | ||
update_container_registry_quota( | ||
scope_id: $scope_id, quota: $quota) { | ||
ok msg | ||
} | ||
} | ||
""" | ||
) | ||
|
||
scope_id = f"project:{group_id}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fregataa If possible, instead of hardcoding the What do you think about moving the types related to |
||
variables = {"scope_id": scope_id, "quota": quota} | ||
data = await api_session.get().Admin._query(query, variables) | ||
return data["update_container_registry_quota"] | ||
|
||
@api_function | ||
@classmethod | ||
async def delete_container_registry_quota(cls, group_id: str) -> dict: | ||
""" | ||
Delete Quota Limit for the group's container registry. | ||
Currently only HarborV2 registry is supported. | ||
|
||
You need an admin privilege for this operation. | ||
""" | ||
query = textwrap.dedent( | ||
"""\ | ||
mutation($scope_id: ScopeField!) { | ||
delete_container_registry_quota( | ||
scope_id: $scope_id) { | ||
ok msg | ||
} | ||
} | ||
""" | ||
) | ||
|
||
scope_id = f"project:{group_id}" | ||
variables = {"scope_id": scope_id} | ||
data = await api_session.get().Admin._query(query, variables) | ||
return data["delete_container_registry_quota"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import TYPE_CHECKING, Any, Iterable, Tuple | ||
|
||
import aiohttp_cors | ||
import trafaret as t | ||
from aiohttp import web | ||
|
||
from ai.backend.common import validators as tx | ||
from ai.backend.logging import BraceStyleAdapter | ||
from ai.backend.manager.models.gql_models.container_registry_utils import ( | ||
HarborQuotaManager, | ||
) | ||
from ai.backend.manager.models.rbac import ProjectScope | ||
|
||
if TYPE_CHECKING: | ||
from .context import RootContext | ||
|
||
from .auth import superadmin_required | ||
from .manager import READ_ALLOWED, server_status_required | ||
from .types import CORSOptions, WebMiddleware | ||
from .utils import check_api_params | ||
|
||
log = BraceStyleAdapter(logging.getLogger(__spec__.name)) | ||
|
||
|
||
@server_status_required(READ_ALLOWED) | ||
@superadmin_required | ||
@check_api_params( | ||
t.Dict({ | ||
tx.AliasedKey(["group_id", "group"]): t.String, | ||
tx.AliasedKey(["quota"]): t.Int, | ||
}) | ||
) | ||
async def update_registry_quota(request: web.Request, params: Any) -> web.Response: | ||
log.info("UPDATE_REGISTRY_QUOTA (gr:{})", params["group_id"]) | ||
root_ctx: RootContext = request.app["_root.context"] | ||
group_id = params["group_id"] | ||
scope_id = ProjectScope(project_id=group_id, domain_name=None) | ||
quota = int(params["quota"]) | ||
|
||
async with root_ctx.db.begin_session() as db_sess: | ||
manager = await HarborQuotaManager.new(db_sess, scope_id) | ||
await manager.update(quota) | ||
|
||
return web.json_response({}) | ||
|
||
|
||
@server_status_required(READ_ALLOWED) | ||
@superadmin_required | ||
@check_api_params( | ||
t.Dict({ | ||
tx.AliasedKey(["group_id", "group"]): t.String, | ||
}) | ||
) | ||
async def delete_registry_quota(request: web.Request, params: Any) -> web.Response: | ||
log.info("DELETE_REGISTRY_QUOTA (gr:{})", params["group_id"]) | ||
root_ctx: RootContext = request.app["_root.context"] | ||
group_id = params["group_id"] | ||
scope_id = ProjectScope(project_id=group_id, domain_name=None) | ||
|
||
async with root_ctx.db.begin_session() as db_sess: | ||
manager = await HarborQuotaManager.new(db_sess, scope_id) | ||
await manager.delete() | ||
|
||
return web.json_response({}) | ||
|
||
|
||
@server_status_required(READ_ALLOWED) | ||
@superadmin_required | ||
@check_api_params( | ||
t.Dict({ | ||
tx.AliasedKey(["group_id", "group"]): t.String, | ||
tx.AliasedKey(["quota"]): t.Int, | ||
}) | ||
) | ||
async def create_registry_quota(request: web.Request, params: Any) -> web.Response: | ||
log.info("CREATE_REGISTRY_QUOTA (gr:{})", params["group_id"]) | ||
root_ctx: RootContext = request.app["_root.context"] | ||
group_id = params["group_id"] | ||
scope_id = ProjectScope(project_id=group_id, domain_name=None) | ||
quota = int(params["quota"]) | ||
|
||
async with root_ctx.db.begin_session() as db_sess: | ||
manager = await HarborQuotaManager.new(db_sess, scope_id) | ||
await manager.create(quota) | ||
|
||
return web.json_response({}) | ||
|
||
|
||
@server_status_required(READ_ALLOWED) | ||
@superadmin_required | ||
@check_api_params( | ||
t.Dict({ | ||
tx.AliasedKey(["group_id", "group"]): t.String, | ||
}) | ||
) | ||
async def read_registry_quota(request: web.Request, params: Any) -> web.Response: | ||
log.info("READ_REGISTRY_QUOTA (gr:{})", params["group_id"]) | ||
root_ctx: RootContext = request.app["_root.context"] | ||
group_id = params["group_id"] | ||
scope_id = ProjectScope(project_id=group_id, domain_name=None) | ||
|
||
async with root_ctx.db.begin_session() as db_sess: | ||
manager = await HarborQuotaManager.new(db_sess, scope_id) | ||
quota = await manager.read() | ||
|
||
return web.json_response({"result": quota}) | ||
|
||
|
||
def create_app( | ||
default_cors_options: CORSOptions, | ||
) -> Tuple[web.Application, Iterable[WebMiddleware]]: | ||
app = web.Application() | ||
app["api_versions"] = (1, 2, 3, 4, 5) | ||
app["prefix"] = "group" | ||
cors = aiohttp_cors.setup(app, defaults=default_cors_options) | ||
cors.add(app.router.add_route("POST", "/registry-quota", create_registry_quota)) | ||
cors.add(app.router.add_route("GET", "/registry-quota", read_registry_quota)) | ||
cors.add(app.router.add_route("PATCH", "/registry-quota", update_registry_quota)) | ||
cors.add(app.router.add_route("DELETE", "/registry-quota", delete_registry_quota)) | ||
Comment on lines
+119
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we use PATCH, DELETE method? @fregataa |
||
return app, [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fregataa Registry quota READ operation can be executed even if the user is not an admin.
However, it seems that GQL queries in the current SDK can only be executed through
Admin
.What do you think about adding this
query
,_query
functions toUser
as well?