From 28bf83835bf3d6e95f54f4a6687aaae9e124e49e Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Wed, 8 Nov 2023 15:32:07 +0900 Subject: [PATCH 01/21] revert functioned description and deprecated msg --- src/ai/backend/manager/models/group.py | 15 ++- .../backend/manager/models/resource_policy.py | 111 ++++++++++-------- src/ai/backend/manager/models/utils.py | 14 --- 3 files changed, 72 insertions(+), 68 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index d97180369e..a54ec65abd 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -393,8 +393,19 @@ class ModifyGroupInput(graphene.InputObjectType): is_active = graphene.Boolean(required=False) domain_name = graphene.String(required=False) total_resource_slots = graphene.JSONString(required=False) - user_update_mode = graphene.String(required=False) - user_uuids = graphene.List(lambda: graphene.String, required=False) + user_update_mode = graphene.String( + deprecation_reason=( + "Deprecated since 24.03.0. Recommend to use CreateUsersToGroup, RemoveUsersFromGroup" + " mutation" + ) + ) + user_uuids = graphene.List( + lambda: graphene.String, + deprecation_reason=( + "Deprecated since 24.03.0. Recommend to use CreateUsersToGroup, RemoveUsersFromGroup" + " mutation" + ), + ) allowed_vfolder_hosts = graphene.JSONString(required=False) integration_id = graphene.String(required=False) resource_policy = graphene.String(required=False) diff --git a/src/ai/backend/manager/models/resource_policy.py b/src/ai/backend/manager/models/resource_policy.py index d484d34d73..d1f537c696 100644 --- a/src/ai/backend/manager/models/resource_policy.py +++ b/src/ai/backend/manager/models/resource_policy.py @@ -28,7 +28,6 @@ ) from .keypair import keypairs from .user import UserRole -from .utils import deprecation_reason_msg, description_msg if TYPE_CHECKING: from .gql import GraphQueryContext @@ -58,34 +57,6 @@ ) -def user_max_vfolder_count(required: bool = False): - return graphene.Int( - required=required, - description=description_msg("24.03.1", "Limitation of the number of user vfolders."), - ) - - -def user_max_quota_scope_size(required: bool = False): - return BigInt( - required=required, - description=description_msg("24.03.1", "Limitation of the quota size of user vfolders."), - ) - - -def project_max_vfolder_count(required: bool = False): - return graphene.Int( - required=required, - description=description_msg("24.03.1", "Limitation of the number of project vfolders."), - ) - - -def project_max_quota_scope_size(required: bool = False): - return BigInt( - required=required, - description=description_msg("24.03.1", "Limitation of the quota size of project vfolders."), - ) - - keypair_resource_policies = sa.Table( "keypair_resource_policies", mapper_registry.metadata, @@ -172,9 +143,9 @@ class KeyPairResourcePolicy(graphene.ObjectType): idle_timeout = BigInt() allowed_vfolder_hosts = graphene.JSONString() - max_vfolder_count = graphene.Int(deprecation_reason=deprecation_reason_msg("23.09.4")) - max_vfolder_size = BigInt(deprecation_reason=deprecation_reason_msg("23.09.4")) - max_quota_scope_size = BigInt(deprecation_reason=deprecation_reason_msg("23.09.4")) + max_vfolder_count = graphene.Int(deprecation_reason="Deprecated since 23.09.4") + max_vfolder_size = BigInt(deprecation_reason="Deprecated since 23.09.4") + max_quota_scope_size = BigInt(deprecation_reason="Deprecated since 23.09.4") @classmethod def from_row( @@ -326,15 +297,15 @@ class CreateKeyPairResourcePolicyInput(graphene.InputObjectType): allowed_vfolder_hosts = graphene.JSONString(required=False) max_vfolder_count = graphene.Int( required=False, - deprecation_reason=deprecation_reason_msg("23.09.4"), + deprecation_reason="Deprecated since 23.09.4", ) max_vfolder_size = BigInt( required=False, - deprecation_reason=deprecation_reason_msg("23.09.4"), + deprecation_reason="Deprecated since 23.09.4", ) max_quota_scope_size = BigInt( required=False, - deprecation_reason=deprecation_reason_msg("23.09.4"), + deprecation_reason="Deprecated since 23.09.4", ) @@ -349,15 +320,15 @@ class ModifyKeyPairResourcePolicyInput(graphene.InputObjectType): allowed_vfolder_hosts = graphene.JSONString(required=False) max_vfolder_count = graphene.Int( required=False, - deprecation_reason=deprecation_reason_msg("23.09.4"), + deprecation_reason="Deprecated since 23.09.4", ) max_vfolder_size = BigInt( required=False, - deprecation_reason=deprecation_reason_msg("23.09.4"), + deprecation_reason="Deprecated since 23.09.4", ) max_quota_scope_size = BigInt( required=False, - deprecation_reason=deprecation_reason_msg("23.09.4"), + deprecation_reason="Deprecated since 23.09.4", ) @@ -471,9 +442,15 @@ class UserResourcePolicy(graphene.ObjectType): id = graphene.ID(required=True) name = graphene.String(required=True) created_at = GQLDateTime(required=True) - max_vfolder_count = user_max_vfolder_count() - max_quota_scope_size = user_max_quota_scope_size() - max_vfolder_size = BigInt(deprecation_reason=deprecation_reason_msg("23.09.1")) + max_vfolder_count = graphene.Int( + required=False, + description="Added since 24.03.1. Limitation of the number of user vfolders.", + ) + max_quota_scope_size = BigInt( + required=False, + description="Added since 24.03.1. Limitation of the quota size of user vfolders.", + ) + max_vfolder_size = BigInt(deprecation_reason="Deprecated since 23.09.1") @classmethod def from_row( @@ -545,13 +522,25 @@ async def batch_load_by_user( class CreateUserResourcePolicyInput(graphene.InputObjectType): - max_vfolder_count = user_max_vfolder_count() - max_quota_scope_size = user_max_quota_scope_size() + max_vfolder_count = graphene.Int( + required=False, + description="Added since 24.03.1. Limitation of the number of user vfolders.", + ) + max_quota_scope_size = BigInt( + required=False, + description="Added since 24.03.1. Limitation of the quota size of user vfolders.", + ) class ModifyUserResourcePolicyInput(graphene.InputObjectType): - max_vfolder_count = user_max_vfolder_count() - max_quota_scope_size = user_max_quota_scope_size() + max_vfolder_count = graphene.Int( + required=False, + description="Added since 24.03.1. Limitation of the number of user vfolders.", + ) + max_quota_scope_size = BigInt( + required=False, + description="Added since 24.03.1. Limitation of the quota size of user vfolders.", + ) class CreateUserResourcePolicy(graphene.Mutation): @@ -648,9 +637,15 @@ class ProjectResourcePolicy(graphene.ObjectType): id = graphene.ID(required=True) name = graphene.String(required=True) created_at = GQLDateTime(required=True) - max_vfolder_count = project_max_vfolder_count() - max_quota_scope_size = project_max_quota_scope_size() - max_vfolder_size = BigInt(deprecation_reason=deprecation_reason_msg("23.09.1")) + max_vfolder_count = graphene.Int( + required=False, + description="Added since 24.03.1. Limitation of the number of project vfolders.", + ) + max_quota_scope_size = BigInt( + required=False, + description="Added since 24.03.1. Limitation of the quota size of project vfolders.", + ) + max_vfolder_size = BigInt(deprecation_reason="Deprecated since 23.09.1") @classmethod def from_row( @@ -723,13 +718,25 @@ async def batch_load_by_project( class CreateProjectResourcePolicyInput(graphene.InputObjectType): - max_vfolder_count = project_max_vfolder_count() - max_quota_scope_size = project_max_quota_scope_size() + max_vfolder_count = graphene.Int( + required=False, + description="Added since 24.03.1. Limitation of the number of project vfolders.", + ) + max_quota_scope_size = BigInt( + required=False, + description="Added since 24.03.1. Limitation of the quota size of project vfolders.", + ) class ModifyProjectResourcePolicyInput(graphene.InputObjectType): - max_vfolder_count = project_max_vfolder_count() - max_quota_scope_size = project_max_quota_scope_size() + max_vfolder_count = graphene.Int( + required=False, + description="Added since 24.03.1. Limitation of the number of project vfolders.", + ) + max_quota_scope_size = BigInt( + required=False, + description="Added since 24.03.1. Limitation of the quota size of project vfolders.", + ) class CreateProjectResourcePolicy(graphene.Mutation): diff --git a/src/ai/backend/manager/models/utils.py b/src/ai/backend/manager/models/utils.py index f0850699eb..0ece7f7e5e 100644 --- a/src/ai/backend/manager/models/utils.py +++ b/src/ai/backend/manager/models/utils.py @@ -362,17 +362,3 @@ def agg_to_array(column: sa.Column) -> sa.sql.functions.Function: def is_db_retry_error(e: Exception) -> bool: return isinstance(e, DBAPIError) and getattr(e.orig, "pgcode", None) == "40001" - - -def description_msg(version: str, detail: str | None = None) -> str: - val = f"Added since {version}." - if detail: - val = f"{val} {detail}" - return val - - -def deprecation_reason_msg(version: str, detail: str | None = None) -> str: - val = f"Deprecated since {version}." - if detail: - val = f"{val} {detail}" - return val From 55d74b800f2106526acbd09610af43047cf146aa Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Wed, 8 Nov 2023 16:19:10 +0900 Subject: [PATCH 02/21] impl user add/delete group mutation --- src/ai/backend/manager/models/gql.py | 12 ++- src/ai/backend/manager/models/group.py | 119 +++++++++++++++++++------ 2 files changed, 103 insertions(+), 28 deletions(-) diff --git a/src/ai/backend/manager/models/gql.py b/src/ai/backend/manager/models/gql.py index a0859d58b1..3e55944476 100644 --- a/src/ai/backend/manager/models/gql.py +++ b/src/ai/backend/manager/models/gql.py @@ -52,7 +52,15 @@ from .base import DataLoaderManager, privileged_query, scoped_query from .domain import CreateDomain, DeleteDomain, Domain, ModifyDomain, PurgeDomain from .endpoint import Endpoint, EndpointList, EndpointToken, EndpointTokenList -from .group import CreateGroup, DeleteGroup, Group, ModifyGroup, PurgeGroup +from .group import ( + AddUserToGroup, + CreateGroup, + DeleteGroup, + DeleteUserFromGroup, + Group, + ModifyGroup, + PurgeGroup, +) from .image import ( AliasImage, ClearImages, @@ -170,6 +178,8 @@ class Mutations(graphene.ObjectType): modify_group = ModifyGroup.Field() delete_group = DeleteGroup.Field() purge_group = PurgeGroup.Field() + add_user_to_group = AddUserToGroup.Field() + delete_user_from_group = DeleteUserFromGroup.Field() # super-admin only create_user = CreateUser.Field() diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index a54ec65abd..76f7f34c1a 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -395,14 +395,14 @@ class ModifyGroupInput(graphene.InputObjectType): total_resource_slots = graphene.JSONString(required=False) user_update_mode = graphene.String( deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use CreateUsersToGroup, RemoveUsersFromGroup" + "Deprecated since 24.03.0. Recommend to use AddUsersToGroup, DeleteUsersFromGroup" " mutation" ) ) user_uuids = graphene.List( lambda: graphene.String, deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use CreateUsersToGroup, RemoveUsersFromGroup" + "Deprecated since 24.03.0. Recommend to use AddUsersToGroup, DeleteUsersFromGroup" " mutation" ), ) @@ -411,6 +411,22 @@ class ModifyGroupInput(graphene.InputObjectType): resource_policy = graphene.String(required=False) +class AddUsersToGroupInput(graphene.InputObjectType): + user_ids = graphene.List( + lambda: graphene.UUID, + required=True, + description="Added since 24.03.1. ID array of the users to be added to the group.", + ) + + +class DeleteUsersFromGroupInput(graphene.InputObjectType): + user_ids = graphene.List( + lambda: graphene.UUID, + required=True, + description="Added since 24.03.1. ID array of the users to be deleted from the group.", + ) + + class CreateGroup(graphene.Mutation): allowed_roles = (UserRole.ADMIN, UserRole.SUPERADMIN) @@ -502,35 +518,18 @@ async def mutate( raise ValueError("invalid user_update_mode") if not props.user_uuids: props.user_update_mode = None - if not data and props.user_update_mode is None: + if not data: return cls(ok=False, msg="nothing to update", group=None) async def _do_mutate() -> ModifyGroup: async with graph_ctx.db.begin() as conn: - # TODO: refactor user addition/removal in groups as separate mutations - # (to apply since 21.09) - if props.user_update_mode == "add": - values = [{"user_id": uuid, "group_id": gid} for uuid in props.user_uuids] - await conn.execute( - sa.insert(association_groups_users).values(values), - ) - elif props.user_update_mode == "remove": - await conn.execute( - sa.delete(association_groups_users).where( - (association_groups_users.c.user_id.in_(props.user_uuids)) - & (association_groups_users.c.group_id == gid), - ), - ) - if data: - result = await conn.execute( - sa.update(groups).values(data).where(groups.c.id == gid).returning(groups), - ) - if result.rowcount > 0: - o = Group.from_row(graph_ctx, result.first()) - return cls(ok=True, msg="success", group=o) - return cls(ok=False, msg="no such group", group=None) - else: # updated association_groups_users table - return cls(ok=True, msg="success", group=None) + result = await conn.execute( + sa.update(groups).values(data).where(groups.c.id == gid).returning(groups), + ) + if result.rowcount > 0: + o = Group.from_row(graph_ctx, result.first()) + return cls(ok=True, msg="success", group=o) + return cls(ok=False, msg="no such group", group=None) try: return await execute_with_retry(_do_mutate) @@ -542,6 +541,72 @@ async def _do_mutate() -> ModifyGroup: return cls(ok=False, msg=f"unexpected error: {e}", group=None) +class AddUserToGroup(graphene.Mutation): + allowed_roles = (UserRole.ADMIN, UserRole.SUPERADMIN) + + class Arguments: + gid = graphene.UUID(required=True) + props = AddUsersToGroupInput(required=True) + + ok = graphene.Boolean() + msg = graphene.String() + + @classmethod + async def mutate( + cls, + root, + info: graphene.ResolveInfo, + gid: uuid.UUID, + props: AddUsersToGroupInput, + ) -> AddUserToGroup: + graph_ctx: GraphQueryContext = info.context + data: dict[str, Any] = {} + set_if_set(props, data, "user_ids") + if not props.user_ids: + return cls(ok=False, msg="empty user id array") + + insert_query = sa.insert(AssocGroupUserRow).values( + [ + { + "user_id": uid, + "group_id": gid, + } + for uid in props.user_ids + ] + ) + return await simple_db_mutate_returning_item(cls, graph_ctx, insert_query, item_cls=Group) + + +class DeleteUserFromGroup(graphene.Mutation): + allowed_roles = (UserRole.ADMIN, UserRole.SUPERADMIN) + + class Arguments: + gid = graphene.UUID(required=True) + props = DeleteUsersFromGroupInput(required=True) + + ok = graphene.Boolean() + msg = graphene.String() + + @classmethod + async def mutate( + cls, + root, + info: graphene.ResolveInfo, + gid: uuid.UUID, + props: DeleteUsersFromGroupInput, + ) -> DeleteUserFromGroup: + graph_ctx: GraphQueryContext = info.context + data: dict[str, Any] = {} + set_if_set(props, data, "user_ids") + if not props.user_ids: + return cls(ok=False, msg="empty user id array") + + delete_query = sa.insert(AssocGroupUserRow).where( + (AssocGroupUserRow.group_id == gid) & (AssocGroupUserRow.user_id.in_(props.user_ids)) + ) + return await simple_db_mutate_returning_item(cls, graph_ctx, delete_query, item_cls=Group) + + class DeleteGroup(graphene.Mutation): """ Instead of deleting the group, just mark it as inactive. From b0cd2a486aed74bd576dc6ea643ec540cf949ce2 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Wed, 8 Nov 2023 16:35:38 +0900 Subject: [PATCH 03/21] replace input type of ID from UUID to String --- src/ai/backend/manager/models/group.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 76f7f34c1a..493e97c8f2 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -413,17 +413,17 @@ class ModifyGroupInput(graphene.InputObjectType): class AddUsersToGroupInput(graphene.InputObjectType): user_ids = graphene.List( - lambda: graphene.UUID, + lambda: graphene.String, required=True, - description="Added since 24.03.1. ID array of the users to be added to the group.", + description="Added since 24.03.0. ID array of the users to be added to the group.", ) class DeleteUsersFromGroupInput(graphene.InputObjectType): user_ids = graphene.List( - lambda: graphene.UUID, + lambda: graphene.String, required=True, - description="Added since 24.03.1. ID array of the users to be deleted from the group.", + description="Added since 24.03.0. ID array of the users to be deleted from the group.", ) @@ -568,7 +568,7 @@ async def mutate( insert_query = sa.insert(AssocGroupUserRow).values( [ { - "user_id": uid, + "user_id": uuid.UUID(uid), "group_id": gid, } for uid in props.user_ids @@ -602,7 +602,8 @@ async def mutate( return cls(ok=False, msg="empty user id array") delete_query = sa.insert(AssocGroupUserRow).where( - (AssocGroupUserRow.group_id == gid) & (AssocGroupUserRow.user_id.in_(props.user_ids)) + (AssocGroupUserRow.group_id == gid) + & (AssocGroupUserRow.user_id.in_([uuid.UUID(uid) for uid in props.user_ids])) ) return await simple_db_mutate_returning_item(cls, graph_ctx, delete_query, item_cls=Group) From e1528e8a0beff2c6eba4d132734ff43e8c82baac Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Wed, 8 Nov 2023 17:02:31 +0900 Subject: [PATCH 04/21] keep the modify function alive --- src/ai/backend/manager/models/group.py | 31 +++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 493e97c8f2..af96f0ac5f 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -518,18 +518,33 @@ async def mutate( raise ValueError("invalid user_update_mode") if not props.user_uuids: props.user_update_mode = None - if not data: + if not data and props.user_update_mode is None: return cls(ok=False, msg="nothing to update", group=None) async def _do_mutate() -> ModifyGroup: async with graph_ctx.db.begin() as conn: - result = await conn.execute( - sa.update(groups).values(data).where(groups.c.id == gid).returning(groups), - ) - if result.rowcount > 0: - o = Group.from_row(graph_ctx, result.first()) - return cls(ok=True, msg="success", group=o) - return cls(ok=False, msg="no such group", group=None) + if props.user_update_mode == "add": + values = [{"user_id": uuid, "group_id": gid} for uuid in props.user_uuids] + await conn.execute( + sa.insert(association_groups_users).values(values), + ) + elif props.user_update_mode == "remove": + await conn.execute( + sa.delete(association_groups_users).where( + (association_groups_users.c.user_id.in_(props.user_uuids)) + & (association_groups_users.c.group_id == gid), + ), + ) + if data: + result = await conn.execute( + sa.update(groups).values(data).where(groups.c.id == gid).returning(groups), + ) + if result.rowcount > 0: + o = Group.from_row(graph_ctx, result.first()) + return cls(ok=True, msg="success", group=o) + return cls(ok=False, msg="no such group", group=None) + else: # updated association_groups_users table + return cls(ok=True, msg="success", group=None) try: return await execute_with_retry(_do_mutate) From 4f19cbc303008f97696a61dc35840d6a9f62f9eb Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Wed, 8 Nov 2023 18:53:22 +0900 Subject: [PATCH 05/21] change mutation name typo --- src/ai/backend/manager/models/gql.py | 4 ++-- src/ai/backend/manager/models/group.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ai/backend/manager/models/gql.py b/src/ai/backend/manager/models/gql.py index 3e55944476..ab8e569ae5 100644 --- a/src/ai/backend/manager/models/gql.py +++ b/src/ai/backend/manager/models/gql.py @@ -56,10 +56,10 @@ AddUserToGroup, CreateGroup, DeleteGroup, - DeleteUserFromGroup, Group, ModifyGroup, PurgeGroup, + RemoveUsersFromGroup, ) from .image import ( AliasImage, @@ -179,7 +179,7 @@ class Mutations(graphene.ObjectType): delete_group = DeleteGroup.Field() purge_group = PurgeGroup.Field() add_user_to_group = AddUserToGroup.Field() - delete_user_from_group = DeleteUserFromGroup.Field() + delete_user_from_group = RemoveUsersFromGroup.Field() # super-admin only create_user = CreateUser.Field() diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index af96f0ac5f..7d2b7ec694 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -395,14 +395,14 @@ class ModifyGroupInput(graphene.InputObjectType): total_resource_slots = graphene.JSONString(required=False) user_update_mode = graphene.String( deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use AddUsersToGroup, DeleteUsersFromGroup" + "Deprecated since 24.03.0. Recommend to use AddUsersToGroup, RemoveUsersFromGroup" " mutation" ) ) user_uuids = graphene.List( lambda: graphene.String, deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use AddUsersToGroup, DeleteUsersFromGroup" + "Deprecated since 24.03.0. Recommend to use AddUsersToGroup, RemoveUsersFromGroup" " mutation" ), ) @@ -419,7 +419,7 @@ class AddUsersToGroupInput(graphene.InputObjectType): ) -class DeleteUsersFromGroupInput(graphene.InputObjectType): +class RemoveUsersFromGroupInput(graphene.InputObjectType): user_ids = graphene.List( lambda: graphene.String, required=True, @@ -592,12 +592,12 @@ async def mutate( return await simple_db_mutate_returning_item(cls, graph_ctx, insert_query, item_cls=Group) -class DeleteUserFromGroup(graphene.Mutation): +class RemoveUsersFromGroup(graphene.Mutation): allowed_roles = (UserRole.ADMIN, UserRole.SUPERADMIN) class Arguments: gid = graphene.UUID(required=True) - props = DeleteUsersFromGroupInput(required=True) + props = RemoveUsersFromGroupInput(required=True) ok = graphene.Boolean() msg = graphene.String() @@ -608,8 +608,8 @@ async def mutate( root, info: graphene.ResolveInfo, gid: uuid.UUID, - props: DeleteUsersFromGroupInput, - ) -> DeleteUserFromGroup: + props: RemoveUsersFromGroupInput, + ) -> RemoveUsersFromGroup: graph_ctx: GraphQueryContext = info.context data: dict[str, Any] = {} set_if_set(props, data, "user_ids") From c75db1e2e6ffed717b57fa919e40d9925cbb096a Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Tue, 14 Nov 2023 14:54:01 +0900 Subject: [PATCH 06/21] impl user add/remove in ModifyGroup mutation and revert description msg usage --- src/ai/backend/manager/models/gql.py | 12 +- src/ai/backend/manager/models/group.py | 133 +++++++---------------- src/ai/backend/manager/models/storage.py | 5 +- 3 files changed, 44 insertions(+), 106 deletions(-) diff --git a/src/ai/backend/manager/models/gql.py b/src/ai/backend/manager/models/gql.py index ab8e569ae5..a0859d58b1 100644 --- a/src/ai/backend/manager/models/gql.py +++ b/src/ai/backend/manager/models/gql.py @@ -52,15 +52,7 @@ from .base import DataLoaderManager, privileged_query, scoped_query from .domain import CreateDomain, DeleteDomain, Domain, ModifyDomain, PurgeDomain from .endpoint import Endpoint, EndpointList, EndpointToken, EndpointTokenList -from .group import ( - AddUserToGroup, - CreateGroup, - DeleteGroup, - Group, - ModifyGroup, - PurgeGroup, - RemoveUsersFromGroup, -) +from .group import CreateGroup, DeleteGroup, Group, ModifyGroup, PurgeGroup from .image import ( AliasImage, ClearImages, @@ -178,8 +170,6 @@ class Mutations(graphene.ObjectType): modify_group = ModifyGroup.Field() delete_group = DeleteGroup.Field() purge_group = PurgeGroup.Field() - add_user_to_group = AddUserToGroup.Field() - delete_user_from_group = RemoveUsersFromGroup.Field() # super-admin only create_user = CreateUser.Field() diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 7d2b7ec694..c17d46a15a 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -46,7 +46,7 @@ simple_db_mutate_returning_item, ) from .storage import StorageSessionManager -from .user import ModifyUserInput, UserRole +from .user import UserRole from .utils import ExtendedAsyncSAEngine, execute_with_retry if TYPE_CHECKING: @@ -395,36 +395,28 @@ class ModifyGroupInput(graphene.InputObjectType): total_resource_slots = graphene.JSONString(required=False) user_update_mode = graphene.String( deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use AddUsersToGroup, RemoveUsersFromGroup" - " mutation" + "Deprecated since 24.03.0. Recommend to use `added_users` and `removed_users` fields" ) ) user_uuids = graphene.List( lambda: graphene.String, deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use AddUsersToGroup, RemoveUsersFromGroup" - " mutation" + "Deprecated since 24.03.0. Recommend to use `added_users` and `removed_users` fields" ), ) - allowed_vfolder_hosts = graphene.JSONString(required=False) - integration_id = graphene.String(required=False) - resource_policy = graphene.String(required=False) - - -class AddUsersToGroupInput(graphene.InputObjectType): - user_ids = graphene.List( + added_users = graphene.List( lambda: graphene.String, - required=True, + required=False, description="Added since 24.03.0. ID array of the users to be added to the group.", ) - - -class RemoveUsersFromGroupInput(graphene.InputObjectType): - user_ids = graphene.List( + removed_users = graphene.List( lambda: graphene.String, - required=True, - description="Added since 24.03.0. ID array of the users to be deleted from the group.", + required=False, + description="Added since 24.03.0. ID array of the users to be removed from the group.", ) + allowed_vfolder_hosts = graphene.JSONString(required=False) + integration_id = graphene.String(required=False) + resource_policy = graphene.String(required=False) class CreateGroup(graphene.Mutation): @@ -494,10 +486,11 @@ async def mutate( root, info: graphene.ResolveInfo, gid: uuid.UUID, - props: ModifyUserInput, + props: ModifyGroupInput, ) -> ModifyGroup: graph_ctx: GraphQueryContext = info.context data: Dict[str, Any] = {} + user_data: dict[str, set] = {} set_if_set(props, data, "name") set_if_set(props, data, "description") set_if_set(props, data, "is_active") @@ -512,31 +505,54 @@ async def mutate( set_if_set(props, data, "integration_id") set_if_set(props, data, "resource_policy") + set_if_set(props, user_data, "added_users", clean_func=set) + set_if_set(props, user_data, "removed_users", clean_func=set) + if "name" in data and _rx_slug.search(data["name"]) is None: raise ValueError("invalid name format. slug format required.") if props.user_update_mode not in (None, Undefined, "add", "remove"): raise ValueError("invalid user_update_mode") if not props.user_uuids: props.user_update_mode = None - if not data and props.user_update_mode is None: + if not data and not user_data and props.user_update_mode in (None, Undefined): return cls(ok=False, msg="nothing to update", group=None) async def _do_mutate() -> ModifyGroup: - async with graph_ctx.db.begin() as conn: + async with graph_ctx.db.begin_session() as db_session: + # Using `user_update_mode` and `user_uuids` is deprecated if props.user_update_mode == "add": values = [{"user_id": uuid, "group_id": gid} for uuid in props.user_uuids] - await conn.execute( + await db_session.execute( sa.insert(association_groups_users).values(values), ) elif props.user_update_mode == "remove": - await conn.execute( + await db_session.execute( sa.delete(association_groups_users).where( (association_groups_users.c.user_id.in_(props.user_uuids)) & (association_groups_users.c.group_id == gid), ), ) + + added_users = user_data.get("added_users") or set() + removed_users = user_data.get("removed_users") or set() + if union := (added_users & removed_users): + raise ValueError( + "Should be no duplicate user id in `added_users` and `removed_users`." + f" (ids: {list(union)})" + ) + if added_users: + values = [{"user_id": uuid, "group_id": gid} for uuid in added_users] + await db_session.execute( + sa.insert(association_groups_users).values(values), + ) + if removed_users: + values = [{"user_id": uuid, "group_id": gid} for uuid in removed_users] + await db_session.execute( + sa.insert(association_groups_users).values(values), + ) + if data: - result = await conn.execute( + result = await db_session.execute( sa.update(groups).values(data).where(groups.c.id == gid).returning(groups), ) if result.rowcount > 0: @@ -556,73 +572,6 @@ async def _do_mutate() -> ModifyGroup: return cls(ok=False, msg=f"unexpected error: {e}", group=None) -class AddUserToGroup(graphene.Mutation): - allowed_roles = (UserRole.ADMIN, UserRole.SUPERADMIN) - - class Arguments: - gid = graphene.UUID(required=True) - props = AddUsersToGroupInput(required=True) - - ok = graphene.Boolean() - msg = graphene.String() - - @classmethod - async def mutate( - cls, - root, - info: graphene.ResolveInfo, - gid: uuid.UUID, - props: AddUsersToGroupInput, - ) -> AddUserToGroup: - graph_ctx: GraphQueryContext = info.context - data: dict[str, Any] = {} - set_if_set(props, data, "user_ids") - if not props.user_ids: - return cls(ok=False, msg="empty user id array") - - insert_query = sa.insert(AssocGroupUserRow).values( - [ - { - "user_id": uuid.UUID(uid), - "group_id": gid, - } - for uid in props.user_ids - ] - ) - return await simple_db_mutate_returning_item(cls, graph_ctx, insert_query, item_cls=Group) - - -class RemoveUsersFromGroup(graphene.Mutation): - allowed_roles = (UserRole.ADMIN, UserRole.SUPERADMIN) - - class Arguments: - gid = graphene.UUID(required=True) - props = RemoveUsersFromGroupInput(required=True) - - ok = graphene.Boolean() - msg = graphene.String() - - @classmethod - async def mutate( - cls, - root, - info: graphene.ResolveInfo, - gid: uuid.UUID, - props: RemoveUsersFromGroupInput, - ) -> RemoveUsersFromGroup: - graph_ctx: GraphQueryContext = info.context - data: dict[str, Any] = {} - set_if_set(props, data, "user_ids") - if not props.user_ids: - return cls(ok=False, msg="empty user id array") - - delete_query = sa.insert(AssocGroupUserRow).where( - (AssocGroupUserRow.group_id == gid) - & (AssocGroupUserRow.user_id.in_([uuid.UUID(uid) for uid in props.user_ids])) - ) - return await simple_db_mutate_returning_item(cls, graph_ctx, delete_query, item_cls=Group) - - class DeleteGroup(graphene.Mutation): """ Instead of deleting the group, just mark it as inactive. diff --git a/src/ai/backend/manager/models/storage.py b/src/ai/backend/manager/models/storage.py index e891513ca7..7cc1939c4e 100644 --- a/src/ai/backend/manager/models/storage.py +++ b/src/ai/backend/manager/models/storage.py @@ -30,7 +30,6 @@ from ..api.exceptions import InvalidAPIParameters, VFolderOperationFailed from ..exceptions import InvalidArgument from .base import Item, PaginatedList -from .utils import description_msg if TYPE_CHECKING: from .gql import GraphQueryContext @@ -209,9 +208,9 @@ class Meta: performance_metric = graphene.JSONString() usage = graphene.JSONString() proxy = graphene.String( - description=description_msg("24.03.0", "Name of the proxy which this volume belongs to.") + description="Added since 24.03.0. Name of the proxy which this volume belongs to." ) - name = graphene.String(description=description_msg("24.03.0", "Name of the storage.")) + name = graphene.String(description="Added since 24.03.0. Name of the storage.") async def resolve_hardware_metadata(self, info: graphene.ResolveInfo) -> HardwareMetadata: ctx: GraphQueryContext = info.context From b0c85d3ac026e3ca3545126c7b87387c9acd8b41 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 24 Nov 2023 12:43:43 +0900 Subject: [PATCH 07/21] add news fragment --- changes/1705.fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1705.fix.md diff --git a/changes/1705.fix.md b/changes/1705.fix.md new file mode 100644 index 0000000000..b948416e55 --- /dev/null +++ b/changes/1705.fix.md @@ -0,0 +1 @@ +Refactor group mutation to add/delete users. From 0760819861b57d60bc4915103b5c2a5c4449f16a Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 24 Nov 2023 12:44:51 +0900 Subject: [PATCH 08/21] change added versions of some fields --- .../backend/manager/models/resource_policy.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ai/backend/manager/models/resource_policy.py b/src/ai/backend/manager/models/resource_policy.py index d1f537c696..d91db9bac3 100644 --- a/src/ai/backend/manager/models/resource_policy.py +++ b/src/ai/backend/manager/models/resource_policy.py @@ -444,11 +444,11 @@ class UserResourcePolicy(graphene.ObjectType): created_at = GQLDateTime(required=True) max_vfolder_count = graphene.Int( required=False, - description="Added since 24.03.1. Limitation of the number of user vfolders.", + description="Added since 24.03.0. Limitation of the number of user vfolders.", ) max_quota_scope_size = BigInt( required=False, - description="Added since 24.03.1. Limitation of the quota size of user vfolders.", + description="Added since 24.03.0. Limitation of the quota size of user vfolders.", ) max_vfolder_size = BigInt(deprecation_reason="Deprecated since 23.09.1") @@ -524,22 +524,22 @@ async def batch_load_by_user( class CreateUserResourcePolicyInput(graphene.InputObjectType): max_vfolder_count = graphene.Int( required=False, - description="Added since 24.03.1. Limitation of the number of user vfolders.", + description="Added since 24.03.0. Limitation of the number of user vfolders.", ) max_quota_scope_size = BigInt( required=False, - description="Added since 24.03.1. Limitation of the quota size of user vfolders.", + description="Added since 24.03.0. Limitation of the quota size of user vfolders.", ) class ModifyUserResourcePolicyInput(graphene.InputObjectType): max_vfolder_count = graphene.Int( required=False, - description="Added since 24.03.1. Limitation of the number of user vfolders.", + description="Added since 24.03.0. Limitation of the number of user vfolders.", ) max_quota_scope_size = BigInt( required=False, - description="Added since 24.03.1. Limitation of the quota size of user vfolders.", + description="Added since 24.03.0. Limitation of the quota size of user vfolders.", ) @@ -639,11 +639,11 @@ class ProjectResourcePolicy(graphene.ObjectType): created_at = GQLDateTime(required=True) max_vfolder_count = graphene.Int( required=False, - description="Added since 24.03.1. Limitation of the number of project vfolders.", + description="Added since 24.03.0. Limitation of the number of project vfolders.", ) max_quota_scope_size = BigInt( required=False, - description="Added since 24.03.1. Limitation of the quota size of project vfolders.", + description="Added since 24.03.0. Limitation of the quota size of project vfolders.", ) max_vfolder_size = BigInt(deprecation_reason="Deprecated since 23.09.1") @@ -720,22 +720,22 @@ async def batch_load_by_project( class CreateProjectResourcePolicyInput(graphene.InputObjectType): max_vfolder_count = graphene.Int( required=False, - description="Added since 24.03.1. Limitation of the number of project vfolders.", + description="Added since 24.03.0. Limitation of the number of project vfolders.", ) max_quota_scope_size = BigInt( required=False, - description="Added since 24.03.1. Limitation of the quota size of project vfolders.", + description="Added since 24.03.0. Limitation of the quota size of project vfolders.", ) class ModifyProjectResourcePolicyInput(graphene.InputObjectType): max_vfolder_count = graphene.Int( required=False, - description="Added since 24.03.1. Limitation of the number of project vfolders.", + description="Added since 24.03.0. Limitation of the number of project vfolders.", ) max_quota_scope_size = BigInt( required=False, - description="Added since 24.03.1. Limitation of the quota size of project vfolders.", + description="Added since 24.03.0. Limitation of the quota size of project vfolders.", ) From fed0bfe16bd61d43abe19f07e7dcd39d191021b1 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 24 Nov 2023 12:47:58 +0900 Subject: [PATCH 09/21] update news fragment --- changes/1705.fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/1705.fix.md b/changes/1705.fix.md index b948416e55..63d859c758 100644 --- a/changes/1705.fix.md +++ b/changes/1705.fix.md @@ -1 +1 @@ -Refactor group mutation to add/delete users. +Refactor group mutation by adding `added_users` and `removed_users` fields to replace `user_update_mode`. From b5c76cce7265deea961f63a69e83ad65a86ea4cd Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Wed, 3 Jan 2024 17:02:19 +0900 Subject: [PATCH 10/21] fix wrong code and change naming --- changes/1705.fix.md | 2 +- src/ai/backend/manager/models/group.py | 34 ++++++++++++++------------ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/changes/1705.fix.md b/changes/1705.fix.md index 63d859c758..3f194ef4f5 100644 --- a/changes/1705.fix.md +++ b/changes/1705.fix.md @@ -1 +1 @@ -Refactor group mutation by adding `added_users` and `removed_users` fields to replace `user_update_mode`. +Refactor group mutation by adding `add_users` and `remove_users` fields to replace `user_update_mode`. diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 8450008df8..27b12b7ee4 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -427,21 +427,21 @@ class ModifyGroupInput(graphene.InputObjectType): total_resource_slots = graphene.JSONString(required=False) user_update_mode = graphene.String( deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use `added_users` and `removed_users` fields" + "Deprecated since 24.03.0. Recommend to use `add_users` and `remove_users` fields" ) ) user_uuids = graphene.List( lambda: graphene.String, deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use `added_users` and `removed_users` fields" + "Deprecated since 24.03.0. Recommend to use `add_users` and `remove_users` fields" ), ) - added_users = graphene.List( + add_users = graphene.List( lambda: graphene.String, required=False, description="Added since 24.03.0. ID array of the users to be added to the group.", ) - removed_users = graphene.List( + remove_users = graphene.List( lambda: graphene.String, required=False, description="Added since 24.03.0. ID array of the users to be removed from the group.", @@ -538,8 +538,8 @@ async def mutate( set_if_set(props, data, "integration_id") set_if_set(props, data, "resource_policy") - set_if_set(props, user_data, "added_users", clean_func=set) - set_if_set(props, user_data, "removed_users", clean_func=set) + set_if_set(props, user_data, "add_users", clean_func=set) + set_if_set(props, user_data, "remove_users", clean_func=set) if "name" in data and _rx_slug.search(data["name"]) is None: raise ValueError("invalid name format. slug format required.") @@ -566,22 +566,26 @@ async def _do_mutate() -> ModifyGroup: ), ) - added_users = user_data.get("added_users") or set() - removed_users = user_data.get("removed_users") or set() - if union := (added_users & removed_users): + add_users = user_data.get("add_users") or set() + remove_users = user_data.get("remove_users") or set() + if union := (add_users & remove_users): raise ValueError( - "Should be no duplicate user id in `added_users` and `removed_users`." + "Should be no duplicate user id in `add_users` and `remove_users`." f" (ids: {list(union)})" ) - if added_users: - values = [{"user_id": uuid, "group_id": gid} for uuid in added_users] + if add_users: + values = [{"user_id": uuid, "group_id": gid} for uuid in add_users] await db_session.execute( sa.insert(association_groups_users).values(values), ) - if removed_users: - values = [{"user_id": uuid, "group_id": gid} for uuid in removed_users] + if remove_users: await db_session.execute( - sa.insert(association_groups_users).values(values), + ( + sa.delete(association_groups_users).where( + (association_groups_users.c.group_id == gid) + & (association_groups_users.c.user_id.in_(remove_users)) + ) + ) ) if data: From 7d8c88fffc08f3eeb3d389efe175f789593ce3c4 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Thu, 18 Apr 2024 13:49:27 +0900 Subject: [PATCH 11/21] update version notation --- src/ai/backend/manager/models/group.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index ac054a24b5..2d7883f382 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -449,25 +449,21 @@ class ModifyGroupInput(graphene.InputObjectType): domain_name = graphene.String(required=False) total_resource_slots = graphene.JSONString(required=False) user_update_mode = graphene.String( - deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use `add_users` and `remove_users` fields" - ) + deprecation_reason=("Deprecated since 24.03.3. Use `add_users` and `remove_users` fields") ) user_uuids = graphene.List( lambda: graphene.String, - deprecation_reason=( - "Deprecated since 24.03.0. Recommend to use `add_users` and `remove_users` fields" - ), + deprecation_reason=("Deprecated since 24.03.3. Use `add_users` and `remove_users` fields"), ) add_users = graphene.List( lambda: graphene.String, required=False, - description="Added since 24.03.0. ID array of the users to be added to the group.", + description="Added in 24.03.3. ID array of the users to be added to the group.", ) remove_users = graphene.List( lambda: graphene.String, required=False, - description="Added since 24.03.0. ID array of the users to be removed from the group.", + description="Added in 24.03.3. ID array of the users to be removed from the group.", ) allowed_vfolder_hosts = graphene.JSONString(required=False) integration_id = graphene.String(required=False) From 20b3787e79f4b03dbdcf5c9722f651902baf50fd Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Thu, 18 Apr 2024 13:51:14 +0900 Subject: [PATCH 12/21] remove use of deprecated fields --- src/ai/backend/manager/models/group.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 2d7883f382..8723ad5a19 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -22,7 +22,6 @@ import sqlalchemy as sa import trafaret as t from graphene.types.datetime import DateTime as GQLDateTime -from graphql import Undefined from sqlalchemy.engine.row import Row from sqlalchemy.ext.asyncio import AsyncConnection as SAConnection from sqlalchemy.orm import relationship @@ -567,29 +566,11 @@ async def mutate( if "name" in data and _rx_slug.search(data["name"]) is None: raise ValueError("invalid name format. slug format required.") - if props.user_update_mode not in (None, Undefined, "add", "remove"): - raise ValueError("invalid user_update_mode") - if not props.user_uuids: - props.user_update_mode = None - if not data and not user_data and props.user_update_mode in (None, Undefined): + if not data and not user_data: return cls(ok=False, msg="nothing to update", group=None) async def _do_mutate() -> ModifyGroup: async with graph_ctx.db.begin_session() as db_session: - # Using `user_update_mode` and `user_uuids` is deprecated - if props.user_update_mode == "add": - values = [{"user_id": uuid, "group_id": gid} for uuid in props.user_uuids] - await db_session.execute( - sa.insert(association_groups_users).values(values), - ) - elif props.user_update_mode == "remove": - await db_session.execute( - sa.delete(association_groups_users).where( - (association_groups_users.c.user_id.in_(props.user_uuids)) - & (association_groups_users.c.group_id == gid), - ), - ) - add_users = user_data.get("add_users") or set() remove_users = user_data.get("remove_users") or set() if union := (add_users & remove_users): From c6f911663e4c6f38a7e9f5ca7ebafac799f06b8d Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Thu, 18 Apr 2024 13:54:40 +0900 Subject: [PATCH 13/21] little change of error msg --- src/ai/backend/manager/models/group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 8723ad5a19..23606ed372 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -575,8 +575,8 @@ async def _do_mutate() -> ModifyGroup: remove_users = user_data.get("remove_users") or set() if union := (add_users & remove_users): raise ValueError( - "Should be no duplicate user id in `add_users` and `remove_users`." - f" (ids: {list(union)})" + "Should be no user IDs included in both `add_users` and `remove_users`." + f" (IDs: {list(union)})" ) if add_users: values = [{"user_id": uuid, "group_id": gid} for uuid in add_users] From 999736bc7a9e33cf9766f6cdbd020927493e7ac8 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Thu, 18 Apr 2024 04:57:17 +0000 Subject: [PATCH 14/21] chore: update GraphQL schema dump --- src/ai/backend/manager/api/schema.graphql | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ai/backend/manager/api/schema.graphql b/src/ai/backend/manager/api/schema.graphql index 076a7ec3bb..9d9b7e10de 100644 --- a/src/ai/backend/manager/api/schema.graphql +++ b/src/ai/backend/manager/api/schema.graphql @@ -1133,8 +1133,14 @@ input ModifyGroupInput { is_active: Boolean domain_name: String total_resource_slots: JSONString - user_update_mode: String - user_uuids: [String] + user_update_mode: String @deprecated(reason: "Deprecated since 24.03.3. Use `add_users` and `remove_users` fields") + user_uuids: [String] @deprecated(reason: "Deprecated since 24.03.3. Use `add_users` and `remove_users` fields") + + """Added in 24.03.3. ID array of the users to be added to the group.""" + add_users: [String] + + """Added in 24.03.3. ID array of the users to be removed from the group.""" + remove_users: [String] allowed_vfolder_hosts: JSONString integration_id: String resource_policy: String From 278d878c548505fbaa0cea610ce7dd95304413a7 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Tue, 23 Apr 2024 16:03:35 +0900 Subject: [PATCH 15/21] update gql fields description --- src/ai/backend/manager/models/group.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 23606ed372..910a7f2f0e 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -448,21 +448,21 @@ class ModifyGroupInput(graphene.InputObjectType): domain_name = graphene.String(required=False) total_resource_slots = graphene.JSONString(required=False) user_update_mode = graphene.String( - deprecation_reason=("Deprecated since 24.03.3. Use `add_users` and `remove_users` fields") + deprecation_reason=("Deprecated since 24.09.0. Use `add_users` and `remove_users` fields") ) user_uuids = graphene.List( lambda: graphene.String, - deprecation_reason=("Deprecated since 24.03.3. Use `add_users` and `remove_users` fields"), + deprecation_reason=("Deprecated since 24.09.0. Use `add_users` and `remove_users` fields"), ) add_users = graphene.List( lambda: graphene.String, required=False, - description="Added in 24.03.3. ID array of the users to be added to the group.", + description="Added in 24.09.0. ID array of the users to be added to the group.", ) remove_users = graphene.List( lambda: graphene.String, required=False, - description="Added in 24.03.3. ID array of the users to be removed from the group.", + description="Added in 24.09.0. ID array of the users to be removed from the group.", ) allowed_vfolder_hosts = graphene.JSONString(required=False) integration_id = graphene.String(required=False) From 0820ce5aaf84dc6b26ae3ce452eb4a16bcee920b Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Tue, 23 Apr 2024 07:06:14 +0000 Subject: [PATCH 16/21] chore: update GraphQL schema dump --- src/ai/backend/manager/api/schema.graphql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai/backend/manager/api/schema.graphql b/src/ai/backend/manager/api/schema.graphql index 9d9b7e10de..9aa3f48723 100644 --- a/src/ai/backend/manager/api/schema.graphql +++ b/src/ai/backend/manager/api/schema.graphql @@ -1133,13 +1133,13 @@ input ModifyGroupInput { is_active: Boolean domain_name: String total_resource_slots: JSONString - user_update_mode: String @deprecated(reason: "Deprecated since 24.03.3. Use `add_users` and `remove_users` fields") - user_uuids: [String] @deprecated(reason: "Deprecated since 24.03.3. Use `add_users` and `remove_users` fields") + user_update_mode: String @deprecated(reason: "Deprecated since 24.09.0. Use `add_users` and `remove_users` fields") + user_uuids: [String] @deprecated(reason: "Deprecated since 24.09.0. Use `add_users` and `remove_users` fields") - """Added in 24.03.3. ID array of the users to be added to the group.""" + """Added in 24.09.0. ID array of the users to be added to the group.""" add_users: [String] - """Added in 24.03.3. ID array of the users to be removed from the group.""" + """Added in 24.09.0. ID array of the users to be removed from the group.""" remove_users: [String] allowed_vfolder_hosts: JSONString integration_id: String From 4352885e7d6ffc025ad18af23189d57d7a5d6d51 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Wed, 8 May 2024 18:27:03 +0900 Subject: [PATCH 17/21] rename fields --- src/ai/backend/manager/models/group.py | 32 +++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index d82384bde8..4f8276189b 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -451,18 +451,22 @@ class ModifyGroupInput(graphene.InputObjectType): domain_name = graphene.String(required=False) total_resource_slots = graphene.JSONString(required=False) user_update_mode = graphene.String( - deprecation_reason=("Deprecated since 24.09.0. Use `add_users` and `remove_users` fields") + deprecation_reason=( + "Deprecated since 24.09.0. Use `users_to_add` and `users_to_remove` fields" + ) ) user_uuids = graphene.List( lambda: graphene.String, - deprecation_reason=("Deprecated since 24.09.0. Use `add_users` and `remove_users` fields"), + deprecation_reason=( + "Deprecated since 24.09.0. Use `users_to_add` and `users_to_remove` fields" + ), ) - add_users = graphene.List( + users_to_add = graphene.List( lambda: graphene.String, required=False, description="Added in 24.09.0. ID array of the users to be added to the group.", ) - remove_users = graphene.List( + users_to_remove = graphene.List( lambda: graphene.String, required=False, description="Added in 24.09.0. ID array of the users to be removed from the group.", @@ -564,8 +568,8 @@ async def mutate( set_if_set(props, data, "resource_policy") set_if_set(props, data, "container_registry") - set_if_set(props, user_data, "add_users", clean_func=set) - set_if_set(props, user_data, "remove_users", clean_func=set) + set_if_set(props, user_data, "users_to_add", clean_func=set) + set_if_set(props, user_data, "users_to_remove", clean_func=set) if "name" in data and _rx_slug.search(data["name"]) is None: raise ValueError("invalid name format. slug format required.") @@ -574,24 +578,24 @@ async def mutate( async def _do_mutate() -> ModifyGroup: async with graph_ctx.db.begin_session() as db_session: - add_users = user_data.get("add_users") or set() - remove_users = user_data.get("remove_users") or set() - if union := (add_users & remove_users): + users_to_add = user_data.get("users_to_add") or set() + users_to_remove = user_data.get("users_to_remove") or set() + if union := (users_to_add & users_to_remove): raise ValueError( - "Should be no user IDs included in both `add_users` and `remove_users`." + "Should be no user IDs included in both `users_to_add` and `users_to_remove`." f" (IDs: {list(union)})" ) - if add_users: - values = [{"user_id": uuid, "group_id": gid} for uuid in add_users] + if users_to_add: + values = [{"user_id": uuid, "group_id": gid} for uuid in users_to_add] await db_session.execute( sa.insert(association_groups_users).values(values), ) - if remove_users: + if users_to_remove: await db_session.execute( ( sa.delete(association_groups_users).where( (association_groups_users.c.group_id == gid) - & (association_groups_users.c.user_id.in_(remove_users)) + & (association_groups_users.c.user_id.in_(users_to_remove)) ) ) ) From a9c05ed07f7268d470841a3753ebd599368a5d6f Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Wed, 8 May 2024 09:29:59 +0000 Subject: [PATCH 18/21] chore: update GraphQL schema dump --- src/ai/backend/manager/api/schema.graphql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai/backend/manager/api/schema.graphql b/src/ai/backend/manager/api/schema.graphql index 562877d2d8..b1fcbcbef6 100644 --- a/src/ai/backend/manager/api/schema.graphql +++ b/src/ai/backend/manager/api/schema.graphql @@ -1139,14 +1139,14 @@ input ModifyGroupInput { is_active: Boolean domain_name: String total_resource_slots: JSONString - user_update_mode: String @deprecated(reason: "Deprecated since 24.09.0. Use `add_users` and `remove_users` fields") - user_uuids: [String] @deprecated(reason: "Deprecated since 24.09.0. Use `add_users` and `remove_users` fields") + user_update_mode: String @deprecated(reason: "Deprecated since 24.09.0. Use `users_to_add` and `users_to_remove` fields") + user_uuids: [String] @deprecated(reason: "Deprecated since 24.09.0. Use `users_to_add` and `users_to_remove` fields") """Added in 24.09.0. ID array of the users to be added to the group.""" - add_users: [String] + users_to_add: [String] """Added in 24.09.0. ID array of the users to be removed from the group.""" - remove_users: [String] + users_to_remove: [String] allowed_vfolder_hosts: JSONString integration_id: String resource_policy: String From 66a68802dd409835c6659cdd2265b5228d925498 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Thu, 9 May 2024 13:24:30 +0900 Subject: [PATCH 19/21] minor update for pythonic code --- src/ai/backend/manager/models/group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 4f8276189b..30dfbcdafb 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -578,8 +578,8 @@ async def mutate( async def _do_mutate() -> ModifyGroup: async with graph_ctx.db.begin_session() as db_session: - users_to_add = user_data.get("users_to_add") or set() - users_to_remove = user_data.get("users_to_remove") or set() + users_to_add = user_data.get("users_to_add", set()) + users_to_remove = user_data.get("users_to_remove", set()) if union := (users_to_add & users_to_remove): raise ValueError( "Should be no user IDs included in both `users_to_add` and `users_to_remove`." From 9e068fd87dd521e2541230ba5f2f3795f0c96d0c Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 10 May 2024 17:00:51 +0900 Subject: [PATCH 20/21] update news fragment --- changes/1705.fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/1705.fix.md b/changes/1705.fix.md index 3f194ef4f5..ff0b0e20e6 100644 --- a/changes/1705.fix.md +++ b/changes/1705.fix.md @@ -1 +1 @@ -Refactor group mutation by adding `add_users` and `remove_users` fields to replace `user_update_mode`. +Refactor group mutation by adding `users_to_add` and `users_to_remove` fields to replace `user_update_mode`. From 56218506f0ce1e571e7834fa1475170a932c2c88 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 10 May 2024 17:08:12 +0900 Subject: [PATCH 21/21] handle duplicate user associating to group --- src/ai/backend/manager/models/group.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ai/backend/manager/models/group.py b/src/ai/backend/manager/models/group.py index 30dfbcdafb..a18e7ed200 100644 --- a/src/ai/backend/manager/models/group.py +++ b/src/ai/backend/manager/models/group.py @@ -587,9 +587,12 @@ async def _do_mutate() -> ModifyGroup: ) if users_to_add: values = [{"user_id": uuid, "group_id": gid} for uuid in users_to_add] - await db_session.execute( - sa.insert(association_groups_users).values(values), - ) + try: + await db_session.execute( + sa.insert(association_groups_users).values(values), + ) + except sa.exc.IntegrityError: + raise ValueError("User already belongs to the given project(user group)") if users_to_remove: await db_session.execute( (