Skip to content

Commit 5fa07ce

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 5fa07ce

13 files changed

Lines changed: 533 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 REST v2 API: superadmin CRUD and paginated search of raw config fragments, plus principal-scoped fragment search (BEP-1052).

docs/manager/rest-reference/openapi.json

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,71 @@
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+
},
560625
"CreateAppConfigAllowListInput": {
561626
"description": "Input for registering a new app config allow-list entry.",
562627
"properties": {
@@ -38828,6 +38893,127 @@
3882838893
"description": "Retrieve aggregate resource capacity/usage across all agents (superadmin only).\n\n**Preconditions:**\n* Superadmin privilege required.\n"
3882938894
}
3883038895
},
38896+
"/v2/app-config-fragments/": {
38897+
"post": {
38898+
"operationId": "v2/app-config-fragments.create",
38899+
"tags": [
38900+
"v2/app-config-fragments"
38901+
],
38902+
"responses": {
38903+
"200": {
38904+
"description": "Successful response"
38905+
}
38906+
},
38907+
"security": [
38908+
{
38909+
"TokenAuth": []
38910+
}
38911+
],
38912+
"requestBody": {
38913+
"content": {
38914+
"application/json": {
38915+
"schema": {
38916+
"$ref": "#/components/schemas/CreateAppConfigFragmentInput"
38917+
}
38918+
}
38919+
}
38920+
},
38921+
"parameters": [],
38922+
"description": "Create a fragment at the caller's authorized scope (auth required, RBAC-gated).\n\n**Preconditions:**\n* User privilege required.\n"
38923+
}
38924+
},
38925+
"/v2/app-config-fragments/{fragment_id}": {
38926+
"get": {
38927+
"operationId": "v2/app-config-fragments.get",
38928+
"tags": [
38929+
"v2/app-config-fragments"
38930+
],
38931+
"responses": {
38932+
"200": {
38933+
"description": "Successful response"
38934+
}
38935+
},
38936+
"security": [
38937+
{
38938+
"TokenAuth": []
38939+
}
38940+
],
38941+
"parameters": [
38942+
{
38943+
"name": "fragment_id",
38944+
"in": "path",
38945+
"required": true,
38946+
"schema": {
38947+
"type": "string"
38948+
}
38949+
}
38950+
],
38951+
"description": "Get a fragment by id (auth required, RBAC-gated).\n\n**Preconditions:**\n* User privilege required.\n"
38952+
},
38953+
"patch": {
38954+
"operationId": "v2/app-config-fragments.update",
38955+
"tags": [
38956+
"v2/app-config-fragments"
38957+
],
38958+
"responses": {
38959+
"200": {
38960+
"description": "Successful response"
38961+
}
38962+
},
38963+
"security": [
38964+
{
38965+
"TokenAuth": []
38966+
}
38967+
],
38968+
"requestBody": {
38969+
"content": {
38970+
"application/json": {
38971+
"schema": {
38972+
"$ref": "#/components/schemas/UpdateAppConfigFragmentInput"
38973+
}
38974+
}
38975+
}
38976+
},
38977+
"parameters": [
38978+
{
38979+
"name": "fragment_id",
38980+
"in": "path",
38981+
"required": true,
38982+
"schema": {
38983+
"type": "string"
38984+
}
38985+
}
38986+
],
38987+
"description": "Update a fragment's config document by id (auth required, RBAC-gated).\n\n**Preconditions:**\n* User privilege required.\n"
38988+
},
38989+
"delete": {
38990+
"operationId": "v2/app-config-fragments.purge",
38991+
"tags": [
38992+
"v2/app-config-fragments"
38993+
],
38994+
"responses": {
38995+
"200": {
38996+
"description": "Successful response"
38997+
}
38998+
},
38999+
"security": [
39000+
{
39001+
"TokenAuth": []
39002+
}
39003+
],
39004+
"parameters": [
39005+
{
39006+
"name": "fragment_id",
39007+
"in": "path",
39008+
"required": true,
39009+
"schema": {
39010+
"type": "string"
39011+
}
39012+
}
39013+
],
39014+
"description": "Purge a fragment by id (auth required, RBAC-gated).\n\n**Preconditions:**\n* User privilege required.\n"
39015+
}
39016+
},
3883139017
"/v2/app-config-allow-list/": {
3883239018
"post": {
3883339019
"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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
"CreateAppConfigFragmentInput",
15+
"PurgeAppConfigFragmentInput",
16+
"UpdateAppConfigFragmentInput",
17+
)
18+
19+
20+
class CreateAppConfigFragmentInput(BaseRequestModel):
21+
"""Input for creating a new app config fragment at a given scope."""
22+
23+
config_name: str = Field(
24+
min_length=1,
25+
max_length=128,
26+
description="Registered config name (FK to app_config_definitions).",
27+
)
28+
scope_type: AppConfigScopeType = Field(
29+
description="Scope the fragment is written at (public | domain | user)."
30+
)
31+
scope_id: str | None = Field(
32+
default=None,
33+
description="Scope identifier: the domain id (domain scope) or the user id (user scope). "
34+
"Null for public scope, which has no owner.",
35+
)
36+
config: dict[str, Any] = Field(description="The fragment's JSON config document.")
37+
38+
@model_validator(mode="after")
39+
def _check_scope_id(self) -> Self:
40+
if self.scope_type is AppConfigScopeType.PUBLIC:
41+
if self.scope_id is not None:
42+
raise ValueError("scope_id must be null for public scope.")
43+
elif not self.scope_id:
44+
raise ValueError("scope_id is required for domain and user scopes.")
45+
return self
46+
47+
48+
class UpdateAppConfigFragmentInput(BaseRequestModel):
49+
"""Input for updating an app config fragment's config document."""
50+
51+
id: UUID = Field(description="App config fragment id to update.")
52+
config: dict[str, Any] = Field(description="The replacement JSON config document.")
53+
54+
55+
class PurgeAppConfigFragmentInput(BaseRequestModel):
56+
"""Input for purging an app config fragment."""
57+
58+
id: UUID = Field(description="App config fragment id to purge.")
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Response DTOs for app_config_fragment v2."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import datetime
6+
from typing import Any
7+
from uuid import UUID
8+
9+
from pydantic import Field
10+
11+
from ai.backend.common.api_handlers import BaseResponseModel
12+
from ai.backend.common.data.app_config.types import AppConfigScopeType
13+
14+
__all__ = (
15+
"AppConfigFragmentNode",
16+
"CreateAppConfigFragmentPayload",
17+
"PurgeAppConfigFragmentPayload",
18+
"UpdateAppConfigFragmentPayload",
19+
)
20+
21+
22+
class AppConfigFragmentNode(BaseResponseModel):
23+
"""Node model representing one app config fragment."""
24+
25+
id: UUID = Field(description="App config fragment UUID.")
26+
config_name: str = Field(description="Config name the fragment belongs to.")
27+
scope_type: AppConfigScopeType = Field(description="Scope the fragment is written at.")
28+
scope_id: str | None = Field(
29+
description="Scope identifier: the domain id or user id; null for public scope."
30+
)
31+
config: dict[str, Any] = Field(description="The fragment's JSON config document.")
32+
created_at: datetime = Field(description="Creation timestamp (UTC).")
33+
updated_at: datetime = Field(description="Last update timestamp (UTC).")
34+
35+
36+
class CreateAppConfigFragmentPayload(BaseResponseModel):
37+
"""Payload for app config fragment creation."""
38+
39+
app_config_fragment: AppConfigFragmentNode = Field(description="Created app config fragment.")
40+
41+
42+
class UpdateAppConfigFragmentPayload(BaseResponseModel):
43+
"""Payload for app config fragment update."""
44+
45+
app_config_fragment: AppConfigFragmentNode = Field(description="Updated app config fragment.")
46+
47+
48+
class PurgeAppConfigFragmentPayload(BaseResponseModel):
49+
"""Payload for app config fragment purge."""
50+
51+
id: UUID = Field(description="UUID of the purged app config fragment.")

src/ai/backend/manager/api/adapters/app_config_fragment/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)