Skip to content
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(BA-145): Implement CRUD API for managing Harbor per-project Quota #3090

Open
wants to merge 73 commits into
base: topic/11-06-feat_implement_per-project_images_api_based_on_rbac
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
aca7eee
feat: Implement management API for controlling Harbor per-project Quota
jopemachine Nov 13, 2024
bd8f95e
chore: Add news fragment
jopemachine Nov 13, 2024
337802f
fix: Disable user quota mutation
jopemachine Nov 13, 2024
0d5cab3
fix: Rename variables
jopemachine Nov 13, 2024
2c6dfa4
feat: Add `registry_quota` to GroupNode
jopemachine Nov 13, 2024
c0342fe
fix: Update `UpdateQuota` mutation
jopemachine Nov 13, 2024
b6c6e69
fix: Update `resolve_registry_quota`
jopemachine Nov 13, 2024
a8b329e
fix: Update `resolve_registry_quota`
jopemachine Nov 14, 2024
9ea60d8
fix: Only authorized groups view the quota
jopemachine Nov 14, 2024
0f8f273
feat: Add CreateQuota, DeleteQuota mutation
jopemachine Nov 14, 2024
27f1096
chore: Rename function
jopemachine Nov 14, 2024
e246353
fix: Add exception handling for each operation
jopemachine Nov 14, 2024
3cb0e29
chore: Update schema
jopemachine Nov 14, 2024
f951552
chore: Update error msg
jopemachine Nov 14, 2024
143522f
chore: Rename variable
jopemachine Nov 14, 2024
4355993
fix: Remove useless strenum
jopemachine Nov 14, 2024
863c1e7
refactor: `mutate_harbor_project_quota`
jopemachine Nov 18, 2024
7d3ea15
refactor: Add read operation handling for code reuse
jopemachine Nov 18, 2024
daabe71
feat: Add SDK for registry quota mutations
jopemachine Nov 18, 2024
2417cb2
fix: Broken CI
jopemachine Nov 18, 2024
d8ac478
feat: Implement REST API
jopemachine Nov 18, 2024
8fe98ac
fix: Wrong exception handling
jopemachine Nov 18, 2024
ac72d81
chore: Update comment
jopemachine Nov 19, 2024
7f11e1c
chore: Rename types
jopemachine Nov 19, 2024
7aec995
chore: update GraphQL schema dump
jopemachine Nov 19, 2024
fe6cf74
chore: Rename news fragment
jopemachine Nov 19, 2024
965b924
fix: Use `BigInt`
jopemachine Nov 20, 2024
629d0bb
chore: update GraphQL schema dump
jopemachine Nov 20, 2024
4859b80
refactor: Add `HarborQuotaManager` *(Reflect feedback)
jopemachine Nov 28, 2024
ba23227
fix: Use BigInt
jopemachine Nov 28, 2024
5622c33
chore: self -> cls
jopemachine Nov 28, 2024
7eb42ae
chore: update GraphQL schema dump
jopemachine Nov 28, 2024
43357ea
fix: Improve exception handling
jopemachine Nov 28, 2024
73ad689
feat: Add `test_harbor_read_project_quota`
jopemachine Nov 28, 2024
d5b1c04
chore: `mock-group` -> `mock_group`
jopemachine Nov 28, 2024
01fa97f
fix: Disjoint `FIXTURES_FOR_HARBOR_CRUD_TEST` from `test_harbor_read_…
jopemachine Nov 28, 2024
bce8f0f
chore: Add registry type
jopemachine Nov 28, 2024
1293569
feat: Add create GQL mutation test case
jopemachine Dec 1, 2024
d4d519e
feat: Add update, delete GQL mutation test cases
jopemachine Dec 2, 2024
418df6b
chore: fix typo
jopemachine Dec 2, 2024
0a1f92b
feat: Add REST API `test_harbor_read_project_quota` test
jopemachine Dec 3, 2024
ece9100
chore: Rename variables
jopemachine Dec 3, 2024
313c437
fix: Add `test_harbor_update_project_quota` test
jopemachine Dec 3, 2024
5be9d88
chore: Hoist the variable
jopemachine Dec 3, 2024
d2db5c4
feat: Add `test_harbor_delete_project_quota` test
jopemachine Dec 3, 2024
1cf38b4
feat: Add `test_harbor_create_project_quota` test
jopemachine Dec 3, 2024
01f3412
fix: Change the `test_harbor_read_project_quota` test location
jopemachine Dec 3, 2024
df45814
fix: Change test code location
jopemachine Dec 3, 2024
6c373ba
fix: Reuse `FIXTURES_FOR_HARBOR_CRUD_TEST`
jopemachine Dec 3, 2024
492f2d9
fix: Broken CI
jopemachine Dec 5, 2024
4d62853
refactor: Add `test_case` parametrize annotation
jopemachine Dec 6, 2024
421ebb2
refactor: `test_group` test cases using `test_case`
jopemachine Dec 8, 2024
d467bad
fix: Broken CI
jopemachine Jan 6, 2025
245ed87
fix: Broken CI
jopemachine Jan 6, 2025
b7a3893
fix: Update milestone
jopemachine Jan 8, 2025
c93ae53
fix: Update milestone
jopemachine Jan 8, 2025
bdb143b
fix: Update milestone
jopemachine Jan 10, 2025
9a59dc9
fix: Remove cyclic import
jopemachine Jan 14, 2025
6363083
docs: Update milestone
jopemachine Jan 14, 2025
3688b17
fix: Delete obsolete mutations
jopemachine Jan 14, 2025
699decd
fix: Broken import
jopemachine Jan 24, 2025
b3aa014
chore: update api schema dump
jopemachine Jan 30, 2025
0ff41e9
refactor: Implement `services_ctx` and refactoring using this
jopemachine Jan 31, 2025
b46a159
refactor: Reflect feedbacks
jopemachine Feb 3, 2025
5e1700f
fix: try to fix CI
jopemachine Feb 5, 2025
8389ce1
fix: Fix Broken CI
jopemachine Feb 5, 2025
6223355
fix: Broken lint
jopemachine Feb 5, 2025
9284c7e
fix: Broken test
jopemachine Feb 5, 2025
4bb2b83
refactor: improve abstraction
jopemachine Feb 6, 2025
0986ae3
refactor: improve abstraction
jopemachine Feb 6, 2025
4db221a
fix: Broken import
jopemachine Feb 6, 2025
d1f41b6
fix: Reflect feedback
jopemachine Feb 6, 2025
a5547cb
fix: Reflect feedback
jopemachine Feb 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/3090.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement CRUD API for managing Harbor per-project Quota.
30 changes: 30 additions & 0 deletions docs/manager/graphql-reference/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,9 @@
"""Added in 24.03.7."""
container_registry: JSONString
scaling_groups: [String]

"""Added in 25.2.0."""
registry_quota: BigInt

Check notice on line 724 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'registry_quota' was added to object type 'GroupNode'

Field 'registry_quota' was added to object type 'GroupNode'
user_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): UserConnection
}

Expand Down Expand Up @@ -1980,6 +1983,15 @@
"""Added in 25.1.0."""
delete_endpoint_auto_scaling_rule_node(id: String!): DeleteEndpointAutoScalingRuleNode

"""Added in 25.2.0."""
create_container_registry_quota(quota: BigInt!, scope_id: ScopeField!): CreateContainerRegistryQuota

Check notice on line 1987 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'create_container_registry_quota' was added to object type 'Mutations'

Field 'create_container_registry_quota' was added to object type 'Mutations'

"""Added in 25.2.0."""
update_container_registry_quota(quota: BigInt!, scope_id: ScopeField!): UpdateContainerRegistryQuota

Check notice on line 1990 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'update_container_registry_quota' was added to object type 'Mutations'

Field 'update_container_registry_quota' was added to object type 'Mutations'

"""Added in 25.2.0."""
delete_container_registry_quota(scope_id: ScopeField!): DeleteContainerRegistryQuota

Check notice on line 1993 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'delete_container_registry_quota' was added to object type 'Mutations'

Field 'delete_container_registry_quota' was added to object type 'Mutations'

"""Deprecated since 24.09.0. use `CreateContainerRegistryNode` instead"""
create_container_registry(hostname: String!, props: CreateContainerRegistryInput!): CreateContainerRegistry

Expand Down Expand Up @@ -2864,6 +2876,24 @@
msg: String
}

"""Added in 25.2.0."""
type CreateContainerRegistryQuota {

Check notice on line 2880 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Type 'CreateContainerRegistryQuota' was added

Type 'CreateContainerRegistryQuota' was added
ok: Boolean
msg: String
}

"""Added in 25.2.0."""
type UpdateContainerRegistryQuota {

Check notice on line 2886 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Type 'UpdateContainerRegistryQuota' was added

Type 'UpdateContainerRegistryQuota' was added
ok: Boolean
msg: String
}

"""Added in 25.2.0."""
type DeleteContainerRegistryQuota {

Check notice on line 2892 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Type 'DeleteContainerRegistryQuota' was added

Type 'DeleteContainerRegistryQuota' was added
ok: Boolean
msg: String
}

"""Deprecated since 24.09.0. use `CreateContainerRegistryNode` instead"""
type CreateContainerRegistry {
container_registry: ContainerRegistry
Expand Down
136 changes: 136 additions & 0 deletions docs/manager/rest-reference/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8491,6 +8491,142 @@
"description": "\n**Preconditions:**\n* Admin privilege required.\n* Manager status required: one of FROZEN, RUNNING\n"
}
},
"/group/registry-quota": {
"post": {
"operationId": "group.create_registry_quota",
"tags": [
"group"
],
"responses": {
"200": {
"description": "Successful response"
}
},
"security": [
{
"TokenAuth": []
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"group_id": {
"type": "string"
},
"quota": {
"type": "integer"
}
},
"required": [
"group_id",
"quota"
]
},
"examples": {}
}
}
},
"parameters": [],
"description": "\n**Preconditions:**\n* Superadmin privilege required.\n* Manager status required: one of FROZEN, RUNNING\n"
},
"get": {
"operationId": "group.read_registry_quota",
"tags": [
"group"
],
"responses": {
"200": {
"description": "Successful response"
}
},
"security": [
{
"TokenAuth": []
}
],
"parameters": [
{
"name": "group_id",
"schema": {
"type": "string"
},
"required": true,
"in": "query"
}
],
"description": "\n**Preconditions:**\n* Superadmin privilege required.\n* Manager status required: one of FROZEN, RUNNING\n"
},
"patch": {
"operationId": "group.update_registry_quota",
"tags": [
"group"
],
"responses": {
"200": {
"description": "Successful response"
}
},
"security": [
{
"TokenAuth": []
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"group_id": {
"type": "string"
},
"quota": {
"type": "integer"
}
},
"required": [
"group_id",
"quota"
]
},
"examples": {}
}
}
},
"parameters": [],
"description": "\n**Preconditions:**\n* Superadmin privilege required.\n* Manager status required: one of FROZEN, RUNNING\n"
},
"delete": {
"operationId": "group.delete_registry_quota",
"tags": [
"group"
],
"responses": {
"200": {
"description": "Successful response"
}
},
"security": [
{
"TokenAuth": []
}
],
"parameters": [
{
"name": "group_id",
"schema": {
"type": "string"
},
"required": true,
"in": "query"
}
],
"description": "\n**Preconditions:**\n* Superadmin privilege required.\n* Manager status required: one of FROZEN, RUNNING\n"
}
},
"/group-config/dotfiles": {
"post": {
"operationId": "group-config.create",
Expand Down
100 changes: 100 additions & 0 deletions src/ai/backend/client/func/group.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import textwrap
from typing import Any, Iterable, Optional, Sequence

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
Expand Down Expand Up @@ -293,3 +295,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)
Copy link
Member Author

@jopemachine jopemachine Nov 18, 2024

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 to User as well?

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}"
Copy link
Member Author

@jopemachine jopemachine Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fregataa If possible, instead of hardcoding the scope_id like this, I’d prefer to create a ProjectScope object and serialize it. However, since the ProjectScope type is currently located under the models directory, this violates the pants visibility rule.

What do you think about moving the types related to ScopeField under the common directory?

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"]
9 changes: 9 additions & 0 deletions src/ai/backend/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,12 @@ def join_non_empty(*args: Optional[str], sep: str) -> str:
"""
filtered_args = [arg for arg in args if arg]
return sep.join(filtered_args)


def b64encode(s: str) -> str:
"""
base64 encoding method of graphql_relay.
Use it in components where the graphql_relay package is unavailable.
"""
b: bytes = s.encode("utf-8") if isinstance(s, str) else s
return base64.b64encode(b).decode("ascii")
1 change: 1 addition & 0 deletions src/ai/backend/manager/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ async def _handle_gql_common(request: web.Request, params: Any) -> ExecutionResu
manager_status=manager_status,
known_slot_types=known_slot_types,
background_task_manager=root_ctx.background_task_manager,
services_ctx=root_ctx.services_ctx,
storage_manager=root_ctx.storage_manager,
registry=root_ctx.registry,
idle_checker_host=root_ctx.idle_checker_host,
Expand Down
2 changes: 1 addition & 1 deletion src/ai/backend/manager/api/container_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def patch_container_registry(
request: web.Request, params: PatchContainerRegistryRequestModel
) -> PatchContainerRegistryResponseModel:
registry_id = uuid.UUID(request.match_info["registry_id"])
log.info("PATCH_CONTAINER_REGISTRY (cr:{})", registry_id)
log.info("PATCH_CONTAINER_REGISTRY (registry:{})", registry_id)
root_ctx: RootContext = request.app["_root.context"]
registry_row_updates = params.model_dump(exclude={"allowed_groups"}, exclude_none=True)

Expand Down
2 changes: 2 additions & 0 deletions src/ai/backend/manager/api/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ai.backend.common.metrics.metric import CommonMetricRegistry
from ai.backend.manager.plugin.network import NetworkPluginContext
from ai.backend.manager.service.base import ServicesContext

if TYPE_CHECKING:
from ai.backend.common.bgtask import BackgroundTaskManager
Expand Down Expand Up @@ -50,6 +51,7 @@ class RootContext(BaseContext):
storage_manager: StorageSessionManager
hook_plugin_ctx: HookPluginContext
network_plugin_ctx: NetworkPluginContext
services_ctx: ServicesContext

registry: AgentRegistry
agent_cache: AgentRPCCache
Expand Down
Loading
Loading