Name the sizes a node can ask for
Raw cpus and gpus are a property of the machines an installation has, so a node written against a cluster quietly stops meaning anything when the cluster is replaced. A node says "gpu-small" instead, and what that is stored here — editable, and read again every time the node is built, so changing the flavor changes what the next run gets. Memory joins the schema properly (`ram`, in MB, accepting "2G"), along with `duration_s` for how long a node is expected to take. That one is recorded and shown and nothing else yet: a statement for whoever is planning around the node, not a limit — the limit is still `timeout`. A flavor and a number for the same thing is refused, compared by value so an editor writing the whole object back with its defaults still round-trips. A name nothing stores is refused at the save, which covers the canvas and `fluksio sync` at once, and deleting one a node still asks for says which node. Four sizes are seeded on an installation that has none, and never re-seeded: re-adding one somebody deliberately removed is an argument nobody wins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6HeySA27EkGANZN95QySW
This commit is contained in:
@@ -57,7 +57,7 @@ from fluksio.flow.store import (
|
||||
LibNotFound,
|
||||
StaleVersion,
|
||||
)
|
||||
from fluksio.models import Message, Run, RunArtifact, RunMetric, RunNode
|
||||
from fluksio.models import Flavor, Message, Run, RunArtifact, RunMetric, RunNode
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/flows", tags=["flows"], dependencies=[Depends(get_current_user)]
|
||||
@@ -368,6 +368,31 @@ def read_flow(name: str, controller: FlowControllerDep) -> Any:
|
||||
return _detail(controller, _read_flow(controller, name))
|
||||
|
||||
|
||||
def _check_flavors(definition: FlowDef) -> None:
|
||||
"""Refuse a size nobody stored, here rather than when the node is built.
|
||||
|
||||
Catching it at the save covers the canvas and ``fluksio sync`` in one
|
||||
place; a node that only finds out at build time is a red node somebody has
|
||||
to go and look at.
|
||||
"""
|
||||
named = {
|
||||
node.resources.flavor
|
||||
for node in definition.nodes
|
||||
if node.resources is not None and node.resources.flavor
|
||||
}
|
||||
if not named:
|
||||
return
|
||||
with Session(engine) as session:
|
||||
known = set(session.exec(select(Flavor.name)).all())
|
||||
unknown = sorted(named - known)
|
||||
if unknown:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"No flavor named '{unknown[0]}' "
|
||||
f"(known: {', '.join(sorted(known)) or 'none'})",
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{name}", response_model=FlowDetail)
|
||||
async def save_flow(
|
||||
name: str,
|
||||
@@ -390,6 +415,8 @@ async def save_flow(
|
||||
if len(duplicates) != len(definition.nodes):
|
||||
raise HTTPException(status_code=400, detail="Node names must be unique")
|
||||
|
||||
_check_flavors(definition)
|
||||
|
||||
try:
|
||||
stored = await run_in_threadpool(
|
||||
controller.store.write_draft, definition, definition.version
|
||||
|
||||
Reference in New Issue
Block a user