6974b3620f
ci / lint (push) Successful in 24s
ci / types (push) Successful in 35s
ci / unit (push) Successful in 26s
ci / security (push) Successful in 41s
ci / dockerfile (push) Successful in 8s
ci / chart (push) Successful in 9s
ci / integration (push) Successful in 46s
ci / image (api) (push) Successful in 2m32s
ci / image (reconciler) (push) Successful in 2m50s
ci / image (worker) (push) Successful in 2m43s
ci / bump (push) Successful in 21s
The three existing entries are sized like real products: `elasticsearch` small asks for 1Gi and medium for 4Gi across three replicas. On this cluster that is a request that never schedules, so provisioning them proves something about the node and nothing about svcforge. podinfo 16Mi/10m — one small Go binary, no dependencies, no PVC nginx 32Mi/10m — recognisable, still small Both are addressed as `oci://`, which is load-bearing rather than cosmetic. An OCI chart is pulled by reference with no `helm repo add` first. The three existing entries name `bitnamilegacy/<chart>`, a classic repo alias that nothing in the worker image configures — so as written they cannot resolve at provision time. OCI is the form that works from a bare container, and it is why the e2e test already provisions podinfo. Chart versions were resolved against the real registries before committing (podinfo 6.7.1, nginx 25.0.14 / app 1.31.3) rather than guessed, since a wrong version fails only at provision time. Also updates the CLI ServiceType enum, the catalog test's expected set, and the service list in the OpenAPI description.
95 lines
3.3 KiB
Python
95 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`, "
|
|
"`podinfo`, `nginx`. "
|
|
"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.")
|