Skip to content

Commit 79dfa66

Browse files
committed
feat(BA-6921): AppConfig fragment write REST v2 API (CRUD)
Raw app_config_fragment write surface as REST v2: create / get / update / purge, backed by a dedicated AppConfigFragmentAdapter. Writes and single-fragment reads are open to any authenticated user and gated by RBAC at the processor (scope / single-entity / bulk validators): a user acts on their own user-scope, a domain admin on their domain's, a superadmin on any (public is superadmin-only). Wires the RBAC validators into AppConfigFragmentProcessors (previously ungated). Base of the split; the fragment search API and the merged-read (resolve) API (#12377) stack on top and reuse this entity's AppConfigFragmentNode DTO.
1 parent 10388d5 commit 79dfa66

13 files changed

Lines changed: 746 additions & 0 deletions

File tree

changes/12928.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add the AppConfig fragment write REST v2 API: create / get / update / purge plus bulk update and bulk delete of raw config fragments, open to any authenticated user and gated by RBAC per scope (BEP-1052).

docs/manager/rest-reference/openapi.json

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,110 @@
557557
"title": "AppConfigScopeType",
558558
"type": "string"
559559
},
560+
"CreateAppConfigFragmentInput": {
561+
"description": "Input for creating a new app config fragment at a given scope.",
562+
"properties": {
563+
"config_name": {
564+
"description": "Registered config name (FK to app_config_definitions).",
565+
"maxLength": 128,
566+
"minLength": 1,
567+
"title": "Config Name",
568+
"type": "string"
569+
},
570+
"scope_type": {
571+
"$ref": "#/components/schemas/AppConfigScopeType",
572+
"description": "Scope the fragment is written at (public | domain | user)."
573+
},
574+
"scope_id": {
575+
"anyOf": [
576+
{
577+
"type": "string"
578+
},
579+
{
580+
"type": "null"
581+
}
582+
],
583+
"default": null,
584+
"description": "Scope identifier: the domain id (domain scope) or the user id (user scope). Null for public scope, which has no owner.",
585+
"title": "Scope Id"
586+
},
587+
"config": {
588+
"additionalProperties": true,
589+
"description": "The fragment's JSON config document.",
590+
"title": "Config",
591+
"type": "object"
592+
}
593+
},
594+
"required": [
595+
"config_name",
596+
"scope_type",
597+
"config"
598+
],
599+
"title": "CreateAppConfigFragmentInput",
600+
"type": "object"
601+
},
602+
"UpdateAppConfigFragmentInput": {
603+
"description": "Input for updating an app config fragment's config document.",
604+
"properties": {
605+
"id": {
606+
"description": "App config fragment id to update.",
607+
"format": "uuid",
608+
"title": "Id",
609+
"type": "string"
610+
},
611+
"config": {
612+
"additionalProperties": true,
613+
"description": "The replacement JSON config document.",
614+
"title": "Config",
615+
"type": "object"
616+
}
617+
},
618+
"required": [
619+
"id",
620+
"config"
621+
],
622+
"title": "UpdateAppConfigFragmentInput",
623+
"type": "object"
624+
},
625+
"BulkUpdateAppConfigFragmentInput": {
626+
"description": "Input for updating many fragments' config documents (per-item partial success).",
627+
"properties": {
628+
"items": {
629+
"description": "Fragments to update, each identified by its id.",
630+
"items": {
631+
"$ref": "#/components/schemas/UpdateAppConfigFragmentInput"
632+
},
633+
"minItems": 1,
634+
"title": "Items",
635+
"type": "array"
636+
}
637+
},
638+
"required": [
639+
"items"
640+
],
641+
"title": "BulkUpdateAppConfigFragmentInput",
642+
"type": "object"
643+
},
644+
"BulkPurgeAppConfigFragmentInput": {
645+
"description": "Input for purging many fragments (per-item partial success).",
646+
"properties": {
647+
"ids": {
648+
"description": "Fragment ids to purge.",
649+
"items": {
650+
"format": "uuid",
651+
"type": "string"
652+
},
653+
"minItems": 1,
654+
"title": "Ids",
655+
"type": "array"
656+
}
657+
},
658+
"required": [
659+
"ids"
660+
],
661+
"title": "BulkPurgeAppConfigFragmentInput",
662+
"type": "object"
663+
},
560664
"CreateAppConfigAllowListInput": {
561665
"description": "Input for registering a new app config allow-list entry.",
562666
"properties": {
@@ -38828,6 +38932,185 @@
3882838932
"description": "Retrieve aggregate resource capacity/usage across all agents (superadmin only).\n\n**Preconditions:**\n* Superadmin privilege required.\n"
3882938933
}
3883038934
},
38935+
"/v2/app-config-fragments/": {
38936+
"post": {
38937+
"operationId": "v2/app-config-fragments.create",
38938+
"tags": [
38939+
"v2/app-config-fragments"
38940+
],
38941+
"responses": {
38942+
"200": {
38943+
"description": "Successful response"
38944+
}
38945+
},
38946+
"security": [
38947+
{
38948+
"TokenAuth": []
38949+
}
38950+
],
38951+
"requestBody": {
38952+
"content": {
38953+
"application/json": {
38954+
"schema": {
38955+
"$ref": "#/components/schemas/CreateAppConfigFragmentInput"
38956+
}
38957+
}
38958+
}
38959+
},
38960+
"parameters": [],
38961+
"description": "Create a fragment at the caller's authorized scope (auth required, RBAC-gated).\n\n**Preconditions:**\n* User privilege required.\n"
38962+
}
38963+
},
38964+
"/v2/app-config-fragments/bulk-update": {
38965+
"post": {
38966+
"operationId": "v2/app-config-fragments.bulk_update",
38967+
"tags": [
38968+
"v2/app-config-fragments"
38969+
],
38970+
"responses": {
38971+
"200": {
38972+
"description": "Successful response"
38973+
}
38974+
},
38975+
"security": [
38976+
{
38977+
"TokenAuth": []
38978+
}
38979+
],
38980+
"requestBody": {
38981+
"content": {
38982+
"application/json": {
38983+
"schema": {
38984+
"$ref": "#/components/schemas/BulkUpdateAppConfigFragmentInput"
38985+
}
38986+
}
38987+
}
38988+
},
38989+
"parameters": [],
38990+
"description": "Update many fragments' configs by id, with per-item partial success (auth, RBAC).\n\n**Preconditions:**\n* User privilege required.\n"
38991+
}
38992+
},
38993+
"/v2/app-config-fragments/bulk-delete": {
38994+
"post": {
38995+
"operationId": "v2/app-config-fragments.bulk_purge",
38996+
"tags": [
38997+
"v2/app-config-fragments"
38998+
],
38999+
"responses": {
39000+
"200": {
39001+
"description": "Successful response"
39002+
}
39003+
},
39004+
"security": [
39005+
{
39006+
"TokenAuth": []
39007+
}
39008+
],
39009+
"requestBody": {
39010+
"content": {
39011+
"application/json": {
39012+
"schema": {
39013+
"$ref": "#/components/schemas/BulkPurgeAppConfigFragmentInput"
39014+
}
39015+
}
39016+
}
39017+
},
39018+
"parameters": [],
39019+
"description": "Purge many fragments by id, with per-item partial success (auth, RBAC).\n\n**Preconditions:**\n* User privilege required.\n"
39020+
}
39021+
},
39022+
"/v2/app-config-fragments/{fragment_id}": {
39023+
"get": {
39024+
"operationId": "v2/app-config-fragments.get",
39025+
"tags": [
39026+
"v2/app-config-fragments"
39027+
],
39028+
"responses": {
39029+
"200": {
39030+
"description": "Successful response"
39031+
}
39032+
},
39033+
"security": [
39034+
{
39035+
"TokenAuth": []
39036+
}
39037+
],
39038+
"parameters": [
39039+
{
39040+
"name": "fragment_id",
39041+
"in": "path",
39042+
"required": true,
39043+
"schema": {
39044+
"type": "string"
39045+
}
39046+
}
39047+
],
39048+
"description": "Get a fragment by id (auth required, RBAC-gated).\n\n**Preconditions:**\n* User privilege required.\n"
39049+
},
39050+
"patch": {
39051+
"operationId": "v2/app-config-fragments.update",
39052+
"tags": [
39053+
"v2/app-config-fragments"
39054+
],
39055+
"responses": {
39056+
"200": {
39057+
"description": "Successful response"
39058+
}
39059+
},
39060+
"security": [
39061+
{
39062+
"TokenAuth": []
39063+
}
39064+
],
39065+
"requestBody": {
39066+
"content": {
39067+
"application/json": {
39068+
"schema": {
39069+
"$ref": "#/components/schemas/UpdateAppConfigFragmentInput"
39070+
}
39071+
}
39072+
}
39073+
},
39074+
"parameters": [
39075+
{
39076+
"name": "fragment_id",
39077+
"in": "path",
39078+
"required": true,
39079+
"schema": {
39080+
"type": "string"
39081+
}
39082+
}
39083+
],
39084+
"description": "Update a fragment's config document by id (auth required, RBAC-gated).\n\n**Preconditions:**\n* User privilege required.\n"
39085+
},
39086+
"delete": {
39087+
"operationId": "v2/app-config-fragments.purge",
39088+
"tags": [
39089+
"v2/app-config-fragments"
39090+
],
39091+
"responses": {
39092+
"200": {
39093+
"description": "Successful response"
39094+
}
39095+
},
39096+
"security": [
39097+
{
39098+
"TokenAuth": []
39099+
}
39100+
],
39101+
"parameters": [
39102+
{
39103+
"name": "fragment_id",
39104+
"in": "path",
39105+
"required": true,
39106+
"schema": {
39107+
"type": "string"
39108+
}
39109+
}
39110+
],
39111+
"description": "Purge a fragment by id (auth required, RBAC-gated).\n\n**Preconditions:**\n* User privilege required.\n"
39112+
}
39113+
},
3883139114
"/v2/app-config-allow-list/": {
3883239115
"post": {
3883339116
"operationId": "v2/app-config-allow-list.admin_create",

src/ai/backend/common/dto/manager/v2/app_config_fragment/__init__.py

Whitespace-only changes.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Request DTOs for app_config_fragment v2."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any, Self
6+
from uuid import UUID
7+
8+
from pydantic import Field, model_validator
9+
10+
from ai.backend.common.api_handlers import BaseRequestModel
11+
from ai.backend.common.data.app_config.types import AppConfigScopeType
12+
13+
__all__ = (
14+
"BulkPurgeAppConfigFragmentInput",
15+
"BulkUpdateAppConfigFragmentInput",
16+
"CreateAppConfigFragmentInput",
17+
"PurgeAppConfigFragmentInput",
18+
"UpdateAppConfigFragmentInput",
19+
)
20+
21+
22+
class CreateAppConfigFragmentInput(BaseRequestModel):
23+
"""Input for creating a new app config fragment at a given scope."""
24+
25+
config_name: str = Field(
26+
min_length=1,
27+
max_length=128,
28+
description="Registered config name (FK to app_config_definitions).",
29+
)
30+
scope_type: AppConfigScopeType = Field(
31+
description="Scope the fragment is written at (public | domain | user)."
32+
)
33+
scope_id: str | None = Field(
34+
default=None,
35+
description="Scope identifier: the domain id (domain scope) or the user id (user scope). "
36+
"Null for public scope, which has no owner.",
37+
)
38+
config: dict[str, Any] = Field(description="The fragment's JSON config document.")
39+
40+
@model_validator(mode="after")
41+
def _check_scope_id(self) -> Self:
42+
if self.scope_type is AppConfigScopeType.PUBLIC:
43+
if self.scope_id is not None:
44+
raise ValueError("scope_id must be null for public scope.")
45+
elif not self.scope_id:
46+
raise ValueError("scope_id is required for domain and user scopes.")
47+
return self
48+
49+
50+
class UpdateAppConfigFragmentInput(BaseRequestModel):
51+
"""Input for updating an app config fragment's config document."""
52+
53+
id: UUID = Field(description="App config fragment id to update.")
54+
config: dict[str, Any] = Field(description="The replacement JSON config document.")
55+
56+
57+
class PurgeAppConfigFragmentInput(BaseRequestModel):
58+
"""Input for purging an app config fragment."""
59+
60+
id: UUID = Field(description="App config fragment id to purge.")
61+
62+
63+
class BulkUpdateAppConfigFragmentInput(BaseRequestModel):
64+
"""Input for updating many fragments' config documents (per-item partial success)."""
65+
66+
items: list[UpdateAppConfigFragmentInput] = Field(
67+
min_length=1, description="Fragments to update, each identified by its id."
68+
)
69+
70+
71+
class BulkPurgeAppConfigFragmentInput(BaseRequestModel):
72+
"""Input for purging many fragments (per-item partial success)."""
73+
74+
ids: list[UUID] = Field(min_length=1, description="Fragment ids to purge.")

0 commit comments

Comments
 (0)