diff --git a/changes/11269.feature.md b/changes/11269.feature.md new file mode 100644 index 00000000000..858d7f1d491 --- /dev/null +++ b/changes/11269.feature.md @@ -0,0 +1 @@ +Add `AppConfigPolicy` GraphQL surface (types / queries / bulk mutations) — admins can create / update / purge / list policies that gate AppConfigFragment writes via Strawberry GQL. diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index 390c36ba601..650ed1f12a5 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -191,6 +191,91 @@ type AddRevisionPayload revision: ModelRevision! } +""" +Added in UNRELEASED. Per-item input for admin bulk create — `config_name` + initial `scope_sources`. +""" +input AdminAppConfigPolicyCreateItemInput + @join__type(graph: STRAWBERRY) +{ + """Unique, immutable policy name.""" + configName: String! + + """Ordered scope chain.""" + scopeSources: [String!]! +} + +""" +Added in UNRELEASED. Per-item input for admin bulk update — target row id + new `scope_sources`. +""" +input AdminAppConfigPolicyUpdateItemInput + @join__type(graph: STRAWBERRY) +{ + """Policy row id.""" + id: UUID! + + """Ordered scope chain.""" + scopeSources: [String!]! +} + +"""Added in UNRELEASED. Payload for `adminBulkCreateAppConfigPolicies`.""" +type AdminBulkCreateAppConfigPoliciesPayload + @join__type(graph: STRAWBERRY) +{ + """Created policies.""" + created: [AppConfigPolicy!]! + + """Per-item failures.""" + failed: [AppConfigPolicyBulkError!]! +} + +"""Added in UNRELEASED. Admin bulk create input for app-config policies.""" +input AdminBulkCreateAppConfigPolicyInput + @join__type(graph: STRAWBERRY) +{ + """Policies to create.""" + items: [AdminAppConfigPolicyCreateItemInput!]! +} + +"""Added in UNRELEASED. Payload for `adminBulkPurgeAppConfigPolicies`.""" +type AdminBulkPurgeAppConfigPoliciesPayload + @join__type(graph: STRAWBERRY) +{ + """Ids of policies actually removed (absent ids no-oped).""" + purgedIds: [UUID!]! + + """Per-item failures.""" + failed: [AppConfigPolicyBulkError!]! +} + +""" +Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on row id). +""" +input AdminBulkPurgeAppConfigPolicyInput + @join__type(graph: STRAWBERRY) +{ + """Policy row ids to purge.""" + ids: [UUID!]! +} + +"""Added in UNRELEASED. Payload for `adminBulkUpdateAppConfigPolicies`.""" +type AdminBulkUpdateAppConfigPoliciesPayload + @join__type(graph: STRAWBERRY) +{ + """Updated policies.""" + updated: [AppConfigPolicy!]! + + """Per-item failures.""" + failed: [AppConfigPolicyBulkError!]! +} + +"""Added in UNRELEASED. Admin bulk update input for app-config policies.""" +input AdminBulkUpdateAppConfigPolicyInput + @join__type(graph: STRAWBERRY) +{ + """Policies to update.""" + items: [AdminAppConfigPolicyUpdateItemInput!]! +} + """Added in 26.4.2. Admin input for creating a keypair for a user.""" input AdminCreateKeypairInput @join__type(graph: STRAWBERRY) @@ -895,6 +980,95 @@ type AllowedResourceGroupsPayload items: [String!]! } +"""Added in UNRELEASED. Scoped app-config policy.""" +type AppConfigPolicy implements Node + @join__implements(graph: STRAWBERRY, interface: "Node") + @join__type(graph: STRAWBERRY) +{ + """The Globally Unique ID of this object""" + id: ID! + + """Unique, immutable policy name.""" + configName: String! + + """Ordered scope chain (low → high merge priority).""" + scopeSources: [String!]! + + """Creation timestamp.""" + createdAt: DateTime! + + """Last update timestamp.""" + updatedAt: DateTime +} + +"""Added in UNRELEASED. Per-item failure info for bulk Policy mutations.""" +type AppConfigPolicyBulkError + @join__type(graph: STRAWBERRY) +{ + """Original position in the input list.""" + index: Int! + + """Reason for the failure.""" + message: String! +} + +""" +Added in UNRELEASED. Connection type for paginated app-config policy results. +""" +type AppConfigPolicyConnection + @join__type(graph: STRAWBERRY) +{ + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [AppConfigPolicyEdge!]! + + """Total number of policies matching the query.""" + count: Int! +} + +"""An edge in a connection.""" +type AppConfigPolicyEdge + @join__type(graph: STRAWBERRY) +{ + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: AppConfigPolicy! +} + +"""Added in UNRELEASED. Filter input for querying app-config policies.""" +input AppConfigPolicyFilter + @join__type(graph: STRAWBERRY) +{ + """Filter by config_name.""" + configName: StringFilter = null +} + +"""Added in UNRELEASED. Specifies ordering for app-config policy results.""" +input AppConfigPolicyOrderBy + @join__type(graph: STRAWBERRY) +{ + """The field to order by.""" + field: AppConfigPolicyOrderField! + + """Sort direction.""" + direction: OrderDirection! = DESC +} + +""" +Added in UNRELEASED. Fields available for ordering app-config policies. +""" +enum AppConfigPolicyOrderField + @join__type(graph: STRAWBERRY) +{ + CONFIG_NAME @join__enumValue(graph: STRAWBERRY) + CREATED_AT @join__enumValue(graph: STRAWBERRY) + UPDATED_AT @join__enumValue(graph: STRAWBERRY) +} + """ Added in 24.09.0. Input for approving an artifact revision. @@ -10867,6 +11041,21 @@ type Mutation """ updateMyAllowedClientIp(input: UpdateMyAllowedClientIPInput!): UpdateMyAllowedClientIPPayload! @join__field(graph: STRAWBERRY) + """ + Added in UNRELEASED. Strict insert keyed on `configName` (admin only, per-item transaction). + """ + adminBulkCreateAppConfigPolicies(input: AdminBulkCreateAppConfigPolicyInput!): AdminBulkCreateAppConfigPoliciesPayload! @join__field(graph: STRAWBERRY) + + """ + Added in UNRELEASED. Replace `scope_sources`; `config_name` is immutable. Admin only, per-item transaction. + """ + adminBulkUpdateAppConfigPolicies(input: AdminBulkUpdateAppConfigPolicyInput!): AdminBulkUpdateAppConfigPoliciesPayload! @join__field(graph: STRAWBERRY) + + """ + Added in UNRELEASED. Hard-delete policies by row id; rows still referenced by fragments surface in `failed`. Admin only. + """ + adminBulkPurgeAppConfigPolicies(input: AdminBulkPurgeAppConfigPolicyInput!): AdminBulkPurgeAppConfigPoliciesPayload! @join__field(graph: STRAWBERRY) + """Added in 26.3.0. Create a new query definition (admin only)""" adminCreatePrometheusQueryPreset(input: CreateQueryDefinitionInput!): CreateQueryDefinitionPayload! @join__field(graph: STRAWBERRY) @@ -13384,6 +13573,16 @@ type Query """ adminImageAliases(filter: ImageV2AliasFilter = null, orderBy: [ImageV2AliasOrderByGQL!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ImageV2AliasConnection @join__field(graph: STRAWBERRY) + """ + Added in UNRELEASED. Get a single app-config policy by row id. Available to any authenticated user. + """ + appConfigPolicy(id: UUID!): AppConfigPolicy @join__field(graph: STRAWBERRY) + + """ + Added in UNRELEASED. List app-config policies with filtering and pagination. Available to any authenticated user. + """ + appConfigPolicies(filter: AppConfigPolicyFilter = null, orderBy: [AppConfigPolicyOrderBy!] = null, first: Int = null, after: String = null, last: Int = null, before: String = null, limit: Int = null, offset: Int = null): AppConfigPolicyConnection! @join__field(graph: STRAWBERRY) + """ Added in 26.4.2. Get a single prometheus query preset by ID. Available to any authenticated user since presets are a shared catalog of metric query templates. """ diff --git a/docs/manager/graphql-reference/v2-schema.graphql b/docs/manager/graphql-reference/v2-schema.graphql index 55d5aab3936..ed4b191cf9e 100644 --- a/docs/manager/graphql-reference/v2-schema.graphql +++ b/docs/manager/graphql-reference/v2-schema.graphql @@ -145,6 +145,75 @@ type AddRevisionPayload { revision: ModelRevision! } +""" +Added in UNRELEASED. Per-item input for admin bulk create — `config_name` + initial `scope_sources`. +""" +input AdminAppConfigPolicyCreateItemInput { + """Unique, immutable policy name.""" + configName: String! + + """Ordered scope chain.""" + scopeSources: [String!]! +} + +""" +Added in UNRELEASED. Per-item input for admin bulk update — target row id + new `scope_sources`. +""" +input AdminAppConfigPolicyUpdateItemInput { + """Policy row id.""" + id: UUID! + + """Ordered scope chain.""" + scopeSources: [String!]! +} + +"""Added in UNRELEASED. Payload for `adminBulkCreateAppConfigPolicies`.""" +type AdminBulkCreateAppConfigPoliciesPayload { + """Created policies.""" + created: [AppConfigPolicy!]! + + """Per-item failures.""" + failed: [AppConfigPolicyBulkError!]! +} + +"""Added in UNRELEASED. Admin bulk create input for app-config policies.""" +input AdminBulkCreateAppConfigPolicyInput { + """Policies to create.""" + items: [AdminAppConfigPolicyCreateItemInput!]! +} + +"""Added in UNRELEASED. Payload for `adminBulkPurgeAppConfigPolicies`.""" +type AdminBulkPurgeAppConfigPoliciesPayload { + """Ids of policies actually removed (absent ids no-oped).""" + purgedIds: [UUID!]! + + """Per-item failures.""" + failed: [AppConfigPolicyBulkError!]! +} + +""" +Added in UNRELEASED. Admin bulk purge input for app-config policies (keyed on row id). +""" +input AdminBulkPurgeAppConfigPolicyInput { + """Policy row ids to purge.""" + ids: [UUID!]! +} + +"""Added in UNRELEASED. Payload for `adminBulkUpdateAppConfigPolicies`.""" +type AdminBulkUpdateAppConfigPoliciesPayload { + """Updated policies.""" + updated: [AppConfigPolicy!]! + + """Per-item failures.""" + failed: [AppConfigPolicyBulkError!]! +} + +"""Added in UNRELEASED. Admin bulk update input for app-config policies.""" +input AdminBulkUpdateAppConfigPolicyInput { + """Policies to update.""" + items: [AdminAppConfigPolicyUpdateItemInput!]! +} + """Added in 26.4.2. Admin input for creating a keypair for a user.""" input AdminCreateKeypairInput { """UUID of the target user.""" @@ -609,6 +678,80 @@ type AllowedResourceGroupsPayload { items: [String!]! } +"""Added in UNRELEASED. Scoped app-config policy.""" +type AppConfigPolicy implements Node { + """The Globally Unique ID of this object""" + id: ID! + + """Unique, immutable policy name.""" + configName: String! + + """Ordered scope chain (low → high merge priority).""" + scopeSources: [String!]! + + """Creation timestamp.""" + createdAt: DateTime! + + """Last update timestamp.""" + updatedAt: DateTime +} + +"""Added in UNRELEASED. Per-item failure info for bulk Policy mutations.""" +type AppConfigPolicyBulkError { + """Original position in the input list.""" + index: Int! + + """Reason for the failure.""" + message: String! +} + +""" +Added in UNRELEASED. Connection type for paginated app-config policy results. +""" +type AppConfigPolicyConnection { + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [AppConfigPolicyEdge!]! + + """Total number of policies matching the query.""" + count: Int! +} + +"""An edge in a connection.""" +type AppConfigPolicyEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: AppConfigPolicy! +} + +"""Added in UNRELEASED. Filter input for querying app-config policies.""" +input AppConfigPolicyFilter { + """Filter by config_name.""" + configName: StringFilter = null +} + +"""Added in UNRELEASED. Specifies ordering for app-config policy results.""" +input AppConfigPolicyOrderBy { + """The field to order by.""" + field: AppConfigPolicyOrderField! + + """Sort direction.""" + direction: OrderDirection! = DESC +} + +""" +Added in UNRELEASED. Fields available for ordering app-config policies. +""" +enum AppConfigPolicyOrderField { + CONFIG_NAME + CREATED_AT + UPDATED_AT +} + """ Added in 24.09.0. Input for approving an artifact revision. @@ -6824,6 +6967,21 @@ type Mutation { """ updateMyAllowedClientIp(input: UpdateMyAllowedClientIPInput!): UpdateMyAllowedClientIPPayload! + """ + Added in UNRELEASED. Strict insert keyed on `configName` (admin only, per-item transaction). + """ + adminBulkCreateAppConfigPolicies(input: AdminBulkCreateAppConfigPolicyInput!): AdminBulkCreateAppConfigPoliciesPayload! + + """ + Added in UNRELEASED. Replace `scope_sources`; `config_name` is immutable. Admin only, per-item transaction. + """ + adminBulkUpdateAppConfigPolicies(input: AdminBulkUpdateAppConfigPolicyInput!): AdminBulkUpdateAppConfigPoliciesPayload! + + """ + Added in UNRELEASED. Hard-delete policies by row id; rows still referenced by fragments surface in `failed`. Admin only. + """ + adminBulkPurgeAppConfigPolicies(input: AdminBulkPurgeAppConfigPolicyInput!): AdminBulkPurgeAppConfigPoliciesPayload! + """Added in 26.3.0. Create a new query definition (admin only)""" adminCreatePrometheusQueryPreset(input: CreateQueryDefinitionInput!): CreateQueryDefinitionPayload! @@ -8556,6 +8714,16 @@ type Query { """ adminImageAliases(filter: ImageV2AliasFilter = null, orderBy: [ImageV2AliasOrderByGQL!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ImageV2AliasConnection + """ + Added in UNRELEASED. Get a single app-config policy by row id. Available to any authenticated user. + """ + appConfigPolicy(id: UUID!): AppConfigPolicy + + """ + Added in UNRELEASED. List app-config policies with filtering and pagination. Available to any authenticated user. + """ + appConfigPolicies(filter: AppConfigPolicyFilter = null, orderBy: [AppConfigPolicyOrderBy!] = null, first: Int = null, after: String = null, last: Int = null, before: String = null, limit: Int = null, offset: Int = null): AppConfigPolicyConnection! + """ Added in 26.4.2. Get a single prometheus query preset by ID. Available to any authenticated user since presets are a shared catalog of metric query templates. """ diff --git a/src/ai/backend/manager/api/gql/app_config_policy/__init__.py b/src/ai/backend/manager/api/gql/app_config_policy/__init__.py new file mode 100644 index 00000000000..266d42bda27 --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/__init__.py @@ -0,0 +1,30 @@ +"""AppConfigPolicy GraphQL API package.""" + +from .resolver import ( + admin_bulk_create_app_config_policies, + admin_bulk_purge_app_config_policies, + admin_bulk_update_app_config_policies, + app_config_policies, + app_config_policy, +) +from .types import ( + AppConfigPolicyFilterGQL, + AppConfigPolicyGQL, + AppConfigPolicyOrderByGQL, + AppConfigPolicyOrderFieldGQL, +) + +__all__ = [ + # Queries + "app_config_policy", + "app_config_policies", + # Bulk mutations (bulk-only) + "admin_bulk_create_app_config_policies", + "admin_bulk_update_app_config_policies", + "admin_bulk_purge_app_config_policies", + # Types + "AppConfigPolicyGQL", + "AppConfigPolicyFilterGQL", + "AppConfigPolicyOrderByGQL", + "AppConfigPolicyOrderFieldGQL", +] diff --git a/src/ai/backend/manager/api/gql/app_config_policy/resolver/__init__.py b/src/ai/backend/manager/api/gql/app_config_policy/resolver/__init__.py new file mode 100644 index 00000000000..b8838da6f34 --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/resolver/__init__.py @@ -0,0 +1,17 @@ +from .mutation import ( + admin_bulk_create_app_config_policies, + admin_bulk_purge_app_config_policies, + admin_bulk_update_app_config_policies, +) +from .query import ( + app_config_policies, + app_config_policy, +) + +__all__ = [ + "admin_bulk_create_app_config_policies", + "admin_bulk_purge_app_config_policies", + "admin_bulk_update_app_config_policies", + "app_config_policies", + "app_config_policy", +] diff --git a/src/ai/backend/manager/api/gql/app_config_policy/resolver/mutation.py b/src/ai/backend/manager/api/gql/app_config_policy/resolver/mutation.py new file mode 100644 index 00000000000..c581b59fe5d --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/resolver/mutation.py @@ -0,0 +1,73 @@ +"""AppConfigPolicy GQL mutation resolvers (bulk-only).""" + +from __future__ import annotations + +from strawberry import Info + +from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION +from ai.backend.manager.api.gql.app_config_policy.types import ( + AdminBulkCreateAppConfigPoliciesPayloadGQL, + AdminBulkCreateAppConfigPolicyInputGQL, + AdminBulkPurgeAppConfigPoliciesPayloadGQL, + AdminBulkPurgeAppConfigPolicyInputGQL, + AdminBulkUpdateAppConfigPoliciesPayloadGQL, + AdminBulkUpdateAppConfigPolicyInputGQL, +) +from ai.backend.manager.api.gql.decorators import ( + BackendAIGQLMeta, + gql_mutation, +) +from ai.backend.manager.api.gql.types import StrawberryGQLContext +from ai.backend.manager.api.gql.utils import check_admin_only + + +@gql_mutation( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Strict insert keyed on `configName` (admin only, per-item transaction).", + ) +) +async def admin_bulk_create_app_config_policies( + info: Info[StrawberryGQLContext], + input: AdminBulkCreateAppConfigPolicyInputGQL, +) -> AdminBulkCreateAppConfigPoliciesPayloadGQL: + """Bulk-create app-config policies; failures surface per-item via `failed`.""" + check_admin_only() + result = await info.context.adapters.app_config_policy.admin_bulk_create(input.to_pydantic()) + return AdminBulkCreateAppConfigPoliciesPayloadGQL.from_pydantic(result) + + +@gql_mutation( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description=( + "Replace `scope_sources`; `config_name` is immutable. Admin only, per-item transaction." + ), + ) +) +async def admin_bulk_update_app_config_policies( + info: Info[StrawberryGQLContext], + input: AdminBulkUpdateAppConfigPolicyInputGQL, +) -> AdminBulkUpdateAppConfigPoliciesPayloadGQL: + """Bulk-replace `scope_sources` per row id; missing-id items surface in `failed`.""" + check_admin_only() + result = await info.context.adapters.app_config_policy.admin_bulk_update(input.to_pydantic()) + return AdminBulkUpdateAppConfigPoliciesPayloadGQL.from_pydantic(result) + + +@gql_mutation( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description=( + "Hard-delete policies by row id; rows still referenced by fragments surface in `failed`. Admin only." + ), + ) +) +async def admin_bulk_purge_app_config_policies( + info: Info[StrawberryGQLContext], + input: AdminBulkPurgeAppConfigPolicyInputGQL, +) -> AdminBulkPurgeAppConfigPoliciesPayloadGQL: + """Bulk-purge app-config policies by row id; absent ids no-op.""" + check_admin_only() + result = await info.context.adapters.app_config_policy.admin_bulk_purge(input.to_pydantic()) + return AdminBulkPurgeAppConfigPoliciesPayloadGQL.from_pydantic(result) diff --git a/src/ai/backend/manager/api/gql/app_config_policy/resolver/query.py b/src/ai/backend/manager/api/gql/app_config_policy/resolver/query.py new file mode 100644 index 00000000000..4c3fc878359 --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/resolver/query.py @@ -0,0 +1,95 @@ +"""AppConfigPolicy GQL query resolvers.""" + +from __future__ import annotations + +from uuid import UUID + +import strawberry +from strawberry import Info + +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + SearchAppConfigPoliciesInput, +) +from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION +from ai.backend.manager.api.gql.app_config_policy.types import ( + AppConfigPolicyConnectionGQL, + AppConfigPolicyEdgeGQL, + AppConfigPolicyFilterGQL, + AppConfigPolicyGQL, + AppConfigPolicyOrderByGQL, +) +from ai.backend.manager.api.gql.base import encode_cursor +from ai.backend.manager.api.gql.decorators import ( + BackendAIGQLMeta, + gql_root_field, +) +from ai.backend.manager.api.gql.types import StrawberryGQLContext + + +@gql_root_field( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description=( + "Get a single app-config policy by row id. Available to any authenticated user." + ), + ) +) # type: ignore[misc] +async def app_config_policy( + info: Info[StrawberryGQLContext], + id: UUID, +) -> AppConfigPolicyGQL | None: + payload = await info.context.adapters.app_config_policy.get(id) + if payload.item is None: + return None + return AppConfigPolicyGQL.from_pydantic(payload.item) + + +@gql_root_field( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description=( + "List app-config policies with filtering and pagination. Available to any " + "authenticated user." + ), + ) +) # type: ignore[misc] +async def app_config_policies( + info: Info[StrawberryGQLContext], + filter: AppConfigPolicyFilterGQL | None = None, + order_by: list[AppConfigPolicyOrderByGQL] | None = None, + first: int | None = None, + after: str | None = None, + last: int | None = None, + before: str | None = None, + limit: int | None = None, + offset: int | None = None, +) -> AppConfigPolicyConnectionGQL: + pydantic_filter = filter.to_pydantic() if filter else None + pydantic_order = [o.to_pydantic() for o in order_by] if order_by else None + + payload = await info.context.adapters.app_config_policy.search( + SearchAppConfigPoliciesInput( + filter=pydantic_filter, + order=pydantic_order, + first=first, + after=after, + last=last, + before=before, + limit=limit, + offset=offset, + ) + ) + nodes = [AppConfigPolicyGQL.from_pydantic(node) for node in payload.items] + edges = [ + AppConfigPolicyEdgeGQL(node=node, cursor=encode_cursor(str(node.id))) for node in nodes + ] + return AppConfigPolicyConnectionGQL( + edges=edges, + page_info=strawberry.relay.PageInfo( + has_next_page=payload.has_next_page, + has_previous_page=payload.has_previous_page, + start_cursor=edges[0].cursor if edges else None, + end_cursor=edges[-1].cursor if edges else None, + ), + count=payload.total_count, + ) diff --git a/src/ai/backend/manager/api/gql/app_config_policy/types/__init__.py b/src/ai/backend/manager/api/gql/app_config_policy/types/__init__.py new file mode 100644 index 00000000000..d01e8289161 --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/types/__init__.py @@ -0,0 +1,41 @@ +from .bulk_inputs import ( + AdminAppConfigPolicyCreateItemInputGQL, + AdminAppConfigPolicyUpdateItemInputGQL, + AdminBulkCreateAppConfigPolicyInputGQL, + AdminBulkPurgeAppConfigPolicyInputGQL, + AdminBulkUpdateAppConfigPolicyInputGQL, +) +from .bulk_payloads import ( + AdminBulkCreateAppConfigPoliciesPayloadGQL, + AdminBulkPurgeAppConfigPoliciesPayloadGQL, + AdminBulkUpdateAppConfigPoliciesPayloadGQL, + AppConfigPolicyBulkErrorGQL, +) +from .filters import ( + AppConfigPolicyFilterGQL, + AppConfigPolicyOrderByGQL, + AppConfigPolicyOrderFieldGQL, +) +from .node import ( + AppConfigPolicyConnectionGQL, + AppConfigPolicyEdgeGQL, + AppConfigPolicyGQL, +) + +__all__ = [ + "AdminAppConfigPolicyCreateItemInputGQL", + "AdminAppConfigPolicyUpdateItemInputGQL", + "AdminBulkCreateAppConfigPoliciesPayloadGQL", + "AdminBulkCreateAppConfigPolicyInputGQL", + "AdminBulkPurgeAppConfigPoliciesPayloadGQL", + "AdminBulkPurgeAppConfigPolicyInputGQL", + "AdminBulkUpdateAppConfigPoliciesPayloadGQL", + "AdminBulkUpdateAppConfigPolicyInputGQL", + "AppConfigPolicyBulkErrorGQL", + "AppConfigPolicyConnectionGQL", + "AppConfigPolicyEdgeGQL", + "AppConfigPolicyFilterGQL", + "AppConfigPolicyGQL", + "AppConfigPolicyOrderByGQL", + "AppConfigPolicyOrderFieldGQL", +] diff --git a/src/ai/backend/manager/api/gql/app_config_policy/types/bulk_inputs.py b/src/ai/backend/manager/api/gql/app_config_policy/types/bulk_inputs.py new file mode 100644 index 00000000000..15fec5cb012 --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/types/bulk_inputs.py @@ -0,0 +1,89 @@ +"""AppConfigPolicy bulk-mutation GQL input types.""" + +from __future__ import annotations + +from uuid import UUID + +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + AdminAppConfigPolicyCreateItemInput as AdminCreateItemInputDTO, +) +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + AdminAppConfigPolicyUpdateItemInput as AdminUpdateItemInputDTO, +) +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + AdminBulkCreateAppConfigPoliciesInput as AdminBulkCreateInputDTO, +) +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + AdminBulkPurgeAppConfigPoliciesInput as AdminBulkPurgeInputDTO, +) +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + AdminBulkUpdateAppConfigPoliciesInput as AdminBulkUpdateInputDTO, +) +from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION +from ai.backend.manager.api.gql.decorators import ( + BackendAIGQLMeta, + gql_field, + gql_pydantic_input, +) +from ai.backend.manager.api.gql.pydantic_compat import PydanticInputMixin + + +@gql_pydantic_input( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Per-item input for admin bulk create — `config_name` + initial `scope_sources`.", + ), + name="AdminAppConfigPolicyCreateItemInput", +) +class AdminAppConfigPolicyCreateItemInputGQL(PydanticInputMixin[AdminCreateItemInputDTO]): + config_name: str = gql_field(description="Unique, immutable policy name.") + scope_sources: list[str] = gql_field(description="Ordered scope chain.") + + +@gql_pydantic_input( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Per-item input for admin bulk update — target row id + new `scope_sources`.", + ), + name="AdminAppConfigPolicyUpdateItemInput", +) +class AdminAppConfigPolicyUpdateItemInputGQL(PydanticInputMixin[AdminUpdateItemInputDTO]): + id: UUID = gql_field(description="Policy row id.") + scope_sources: list[str] = gql_field(description="Ordered scope chain.") + + +@gql_pydantic_input( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Admin bulk create input for app-config policies.", + ), + name="AdminBulkCreateAppConfigPolicyInput", +) +class AdminBulkCreateAppConfigPolicyInputGQL(PydanticInputMixin[AdminBulkCreateInputDTO]): + items: list[AdminAppConfigPolicyCreateItemInputGQL] = gql_field( + description="Policies to create." + ) + + +@gql_pydantic_input( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Admin bulk update input for app-config policies.", + ), + name="AdminBulkUpdateAppConfigPolicyInput", +) +class AdminBulkUpdateAppConfigPolicyInputGQL(PydanticInputMixin[AdminBulkUpdateInputDTO]): + items: list[AdminAppConfigPolicyUpdateItemInputGQL] = gql_field( + description="Policies to update." + ) + + +@gql_pydantic_input( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Admin bulk purge input for app-config policies (keyed on row id).", + ), + name="AdminBulkPurgeAppConfigPolicyInput", +) +class AdminBulkPurgeAppConfigPolicyInputGQL(PydanticInputMixin[AdminBulkPurgeInputDTO]): + ids: list[UUID] = gql_field(description="Policy row ids to purge.") diff --git a/src/ai/backend/manager/api/gql/app_config_policy/types/bulk_payloads.py b/src/ai/backend/manager/api/gql/app_config_policy/types/bulk_payloads.py new file mode 100644 index 00000000000..830ad7f925e --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/types/bulk_payloads.py @@ -0,0 +1,80 @@ +"""AppConfigPolicy bulk-mutation GQL payload types.""" + +from __future__ import annotations + +from uuid import UUID + +from ai.backend.common.dto.manager.v2.app_config_policy.response import ( + AdminBulkCreateAppConfigPoliciesPayload as AdminBulkCreatePayloadDTO, +) +from ai.backend.common.dto.manager.v2.app_config_policy.response import ( + AdminBulkPurgeAppConfigPoliciesPayload as AdminBulkPurgePayloadDTO, +) +from ai.backend.common.dto.manager.v2.app_config_policy.response import ( + AdminBulkUpdateAppConfigPoliciesPayload as AdminBulkUpdatePayloadDTO, +) +from ai.backend.common.dto.manager.v2.app_config_policy.response import ( + AppConfigPolicyBulkError as AppConfigPolicyBulkErrorDTO, +) +from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION +from ai.backend.manager.api.gql.app_config_policy.types.node import AppConfigPolicyGQL +from ai.backend.manager.api.gql.decorators import ( + BackendAIGQLMeta, + gql_field, + gql_pydantic_type, +) +from ai.backend.manager.api.gql.pydantic_compat import PydanticOutputMixin + + +@gql_pydantic_type( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Per-item failure info for bulk Policy mutations.", + ), + model=AppConfigPolicyBulkErrorDTO, + name="AppConfigPolicyBulkError", +) +class AppConfigPolicyBulkErrorGQL(PydanticOutputMixin[AppConfigPolicyBulkErrorDTO]): + index: int = gql_field(description="Original position in the input list.") + message: str = gql_field(description="Reason for the failure.") + + +@gql_pydantic_type( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Payload for `adminBulkCreateAppConfigPolicies`.", + ), + model=AdminBulkCreatePayloadDTO, + name="AdminBulkCreateAppConfigPoliciesPayload", +) +class AdminBulkCreateAppConfigPoliciesPayloadGQL(PydanticOutputMixin[AdminBulkCreatePayloadDTO]): + created: list[AppConfigPolicyGQL] = gql_field(description="Created policies.") + failed: list[AppConfigPolicyBulkErrorGQL] = gql_field(description="Per-item failures.") + + +@gql_pydantic_type( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Payload for `adminBulkUpdateAppConfigPolicies`.", + ), + model=AdminBulkUpdatePayloadDTO, + name="AdminBulkUpdateAppConfigPoliciesPayload", +) +class AdminBulkUpdateAppConfigPoliciesPayloadGQL(PydanticOutputMixin[AdminBulkUpdatePayloadDTO]): + updated: list[AppConfigPolicyGQL] = gql_field(description="Updated policies.") + failed: list[AppConfigPolicyBulkErrorGQL] = gql_field(description="Per-item failures.") + + +@gql_pydantic_type( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Payload for `adminBulkPurgeAppConfigPolicies`.", + ), + model=AdminBulkPurgePayloadDTO, + name="AdminBulkPurgeAppConfigPoliciesPayload", +) +class AdminBulkPurgeAppConfigPoliciesPayloadGQL(PydanticOutputMixin[AdminBulkPurgePayloadDTO]): + purged_ids: list[UUID] = gql_field( + description="Ids of policies actually removed (absent ids no-oped).", + ) + failed: list[AppConfigPolicyBulkErrorGQL] = gql_field(description="Per-item failures.") diff --git a/src/ai/backend/manager/api/gql/app_config_policy/types/filters.py b/src/ai/backend/manager/api/gql/app_config_policy/types/filters.py new file mode 100644 index 00000000000..7a03e44294a --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/types/filters.py @@ -0,0 +1,63 @@ +"""AppConfigPolicy GQL filter / order types.""" + +from __future__ import annotations + +from enum import StrEnum + +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + AppConfigPolicyFilter as AppConfigPolicyFilterDTO, +) +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + AppConfigPolicyOrder as AppConfigPolicyOrderDTO, +) +from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION +from ai.backend.manager.api.gql.base import OrderDirection, StringFilter +from ai.backend.manager.api.gql.decorators import ( + BackendAIGQLMeta, + gql_enum, + gql_field, + gql_pydantic_input, +) +from ai.backend.manager.api.gql.pydantic_compat import PydanticInputMixin + + +@gql_pydantic_input( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Filter input for querying app-config policies.", + ), + name="AppConfigPolicyFilter", +) +class AppConfigPolicyFilterGQL(PydanticInputMixin[AppConfigPolicyFilterDTO]): + config_name: StringFilter | None = gql_field( + description="Filter by config_name.", + default=None, + ) + + +@gql_enum( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Fields available for ordering app-config policies.", + ), + name="AppConfigPolicyOrderField", +) +class AppConfigPolicyOrderFieldGQL(StrEnum): + CONFIG_NAME = "config_name" + CREATED_AT = "created_at" + UPDATED_AT = "updated_at" + + +@gql_pydantic_input( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Specifies ordering for app-config policy results.", + ), + name="AppConfigPolicyOrderBy", +) +class AppConfigPolicyOrderByGQL(PydanticInputMixin[AppConfigPolicyOrderDTO]): + field: AppConfigPolicyOrderFieldGQL = gql_field(description="The field to order by.") + direction: OrderDirection = gql_field( + description="Sort direction.", + default=OrderDirection.DESC, + ) diff --git a/src/ai/backend/manager/api/gql/app_config_policy/types/node.py b/src/ai/backend/manager/api/gql/app_config_policy/types/node.py new file mode 100644 index 00000000000..58f35896049 --- /dev/null +++ b/src/ai/backend/manager/api/gql/app_config_policy/types/node.py @@ -0,0 +1,53 @@ +"""AppConfigPolicy GraphQL Node, Edge, and Connection types.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from strawberry.relay import Connection, Edge, NodeID + +from ai.backend.common.dto.manager.v2.app_config_policy.response import AppConfigPolicyNode +from ai.backend.common.meta.meta import NEXT_RELEASE_VERSION +from ai.backend.manager.api.gql.decorators import ( + BackendAIGQLMeta, + gql_connection_type, + gql_field, + gql_node_type, +) +from ai.backend.manager.api.gql.pydantic_compat import PydanticNodeMixin + + +@gql_node_type( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Scoped app-config policy.", + ), + name="AppConfigPolicy", +) +class AppConfigPolicyGQL(PydanticNodeMixin[AppConfigPolicyNode]): + id: NodeID[str] = gql_field(description="Policy row UUID.") + config_name: str = gql_field(description="Unique, immutable policy name.") + scope_sources: list[str] = gql_field( + description="Ordered scope chain (low → high merge priority).", + ) + created_at: datetime = gql_field(description="Creation timestamp.") + updated_at: datetime | None = gql_field(description="Last update timestamp.") + + +AppConfigPolicyEdgeGQL = Edge[AppConfigPolicyGQL] + + +@gql_connection_type( + BackendAIGQLMeta( + added_version=NEXT_RELEASE_VERSION, + description="Connection type for paginated app-config policy results.", + ), + name="AppConfigPolicyConnection", +) +class AppConfigPolicyConnectionGQL(Connection[AppConfigPolicyGQL]): + count: int = gql_field(description="Total number of policies matching the query.") + + def __init__(self, *args: Any, count: int, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.count = count diff --git a/src/ai/backend/manager/api/gql/schema.py b/src/ai/backend/manager/api/gql/schema.py index ae998ff6675..55a50492e72 100644 --- a/src/ai/backend/manager/api/gql/schema.py +++ b/src/ai/backend/manager/api/gql/schema.py @@ -15,6 +15,13 @@ agent_stats, agents_v2, ) +from .app_config_policy import ( + admin_bulk_create_app_config_policies, + admin_bulk_purge_app_config_policies, + admin_bulk_update_app_config_policies, + app_config_policies, + app_config_policy, +) from .artifact import ( approve_artifact_revision, artifact, @@ -505,6 +512,9 @@ class Query: resource_slot_type = resource_slot_type resource_slot_types = resource_slot_types admin_image_aliases = admin_image_aliases + # App Config Policy APIs (read available to any authenticated user) + app_config_policy = app_config_policy + app_config_policies = app_config_policies # Prometheus Query Preset APIs (read available to any authenticated user) prometheus_query_preset = prometheus_query_preset prometheus_query_presets = prometheus_query_presets @@ -780,6 +790,10 @@ class Mutation: admin_unblock_user = admin_unblock_user # IP allowlist self-service mutation update_my_allowed_client_ip = update_my_allowed_client_ip + # App Config Policy - Bulk admin mutations (bulk-only) + admin_bulk_create_app_config_policies = admin_bulk_create_app_config_policies + admin_bulk_update_app_config_policies = admin_bulk_update_app_config_policies + admin_bulk_purge_app_config_policies = admin_bulk_purge_app_config_policies # Prometheus Query Preset - Admin APIs admin_create_prometheus_query_preset = admin_create_prometheus_query_preset admin_modify_prometheus_query_preset = admin_modify_prometheus_query_preset