bb8b14ef03
ci / lint (push) Failing after 56s
ci / types (push) Has been skipped
ci / unit (push) Has been skipped
ci / integration (push) Has been skipped
ci / security (push) Has been skipped
ci / dockerfile (push) Has been skipped
ci / chart (push) Has been skipped
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped
FastAPI already served /docs, /redoc and /openapi.json, and the ingress already
passed them through — the mechanism was there, the content was not. The schema
alone cannot tell a caller the three things they most need to know, and the
route docstrings explain implementation reasoning to a maintainer rather than
usage to a consumer.
Added to the spec itself, so it travels with the API rather than living in a
README the caller does not have:
- An app description covering bearer auth, that every write is 202 + poll,
the instance lifecycle, and the uniform {"code", "message"} error body.
- Tag descriptions for `instances` and `ops`.
- Field descriptions and worked examples on CreateInstanceRequest,
InstanceResponse and ErrorBody, so /docs shows a valid payload instead of
leaving callers to infer one.
The bearer scheme was already exposed via HTTPBearer, which is what makes the
Authorize button in /docs work; there is now a test asserting it stays, along
with the description, the tags and the request example. Docs that are not
tested rot silently, and this is the artifact other teams integrate against.
README documents the three URLs and how to generate a client from the spec.
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
"""Wire types.
|
|
|
|
These are deliberately NOT the domain models. `Instance` carries `team`, `namespace` and
|
|
`release_name` — placement details a tenant has no business seeing and no business
|
|
setting. The response model is the allow-list that keeps them off the wire, which is why
|
|
it is written out by hand instead of derived from `Instance`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from svcforge_core.domain.states import InstanceState
|
|
|
|
|
|
class CreateInstanceRequest(BaseModel):
|
|
"""What a tenant may ask for.
|
|
|
|
`service_type` and `size` are plain strings, not enums: the catalog is data loaded at
|
|
runtime, so baking its keys into a type would mean a redeploy to add a service type,
|
|
and a 422 (schema) where the spec wants a 404 (unknown resource). They are validated
|
|
against the catalog in the handler.
|
|
"""
|
|
|
|
model_config = ConfigDict(
|
|
extra="forbid",
|
|
json_schema_extra={"examples": [{"service_type": "redis", "size": "small", "ttl_days": 7}]},
|
|
)
|
|
|
|
service_type: str = Field(
|
|
min_length=1,
|
|
description=(
|
|
"A service type in the catalog, e.g. `elasticsearch`, `redis`, `postgres`. "
|
|
"Unknown values return 404."
|
|
),
|
|
)
|
|
size: str = Field(
|
|
description=(
|
|
"A size the catalog defines for that service type, e.g. `small`. Unknown values return 422."
|
|
),
|
|
)
|
|
ttl_days: int | None = Field(
|
|
default=None,
|
|
ge=1,
|
|
le=30,
|
|
description="Delete the instance automatically after this many days. Omit for no expiry.",
|
|
)
|
|
|
|
|
|
class InstanceResponse(BaseModel):
|
|
"""What a tenant gets back. A subset of Instance, on purpose."""
|
|
|
|
model_config = ConfigDict(
|
|
from_attributes=True,
|
|
json_schema_extra={
|
|
"examples": [
|
|
{
|
|
"id": "0f8b7d3e-1c2a-4f5b-9e6d-7a8b9c0d1e2f",
|
|
"state": "ready",
|
|
"service_type": "redis",
|
|
"size": "small",
|
|
"endpoint": "http://acme-redis-0f8b7d3e.tenant-acme.svc.cluster.local",
|
|
"chart_version": "20.6.2",
|
|
"error": None,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
id: UUID = Field(description="Poll `GET /v1/instances/{id}` with this to watch the state change.")
|
|
state: InstanceState = Field(description="Lifecycle state. Only `ready` carries a usable endpoint.")
|
|
service_type: str
|
|
size: str
|
|
endpoint: str | None = Field(description="In-cluster DNS name. Null until the instance is `ready`.")
|
|
chart_version: str = Field(
|
|
description="The chart version actually deployed, written only after helm succeeds."
|
|
)
|
|
error: str | None = Field(description="Why the last attempt failed. Null unless `state` is `failed`.")
|
|
|
|
|
|
class ErrorBody(BaseModel):
|
|
"""Every non-2xx body. `code` is for machines, `message` is for humans."""
|
|
|
|
model_config = ConfigDict(
|
|
json_schema_extra={
|
|
"examples": [{"code": "unknown_service_type", "message": "no such service_type: mongodb"}]
|
|
}
|
|
)
|
|
|
|
code: str = Field(description="Stable machine-readable identifier for the failure.")
|
|
message: str = Field(description="Human-readable detail. Do not parse this.")
|