6033967e6a
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm packaging, API contract packs, and pytest/Pulumi coverage labs.
269 lines
8.8 KiB
Python
269 lines
8.8 KiB
Python
"""Heat Orchestration API v1."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Annotated, Any
|
|
from uuid import uuid4
|
|
|
|
from asyncpg import Connection
|
|
from fastapi import APIRouter, Depends, Request, Response
|
|
|
|
from app.openstack.auth import TokenContext
|
|
from app.openstack.deps import get_conn, require_project_token, require_token
|
|
from app.openstack.errors import OpenStackError
|
|
|
|
router = APIRouter(tags=["Heat"])
|
|
|
|
|
|
def _stack(row: Any) -> dict[str, Any]:
|
|
return {
|
|
"id": str(row["id"]),
|
|
"stack_name": row["stack_name"],
|
|
"stack_status": row["stack_status"],
|
|
"description": row["description"],
|
|
"creation_time": row["created_at"].strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"updated_time": row["updated_at"].strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"stack_owner": str(row["project_id"]),
|
|
"parent": None,
|
|
"stack_user_project_id": str(row["project_id"]),
|
|
"outputs": row["outputs"]
|
|
if not isinstance(row["outputs"], str)
|
|
else json.loads(row["outputs"]),
|
|
"parameters": row["parameters"]
|
|
if not isinstance(row["parameters"], str)
|
|
else json.loads(row["parameters"]),
|
|
"links": [
|
|
{
|
|
"rel": "self",
|
|
"href": f"/v1/{row['project_id']}/stacks/{row['stack_name']}/{row['id']}",
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
@router.get("/v1")
|
|
@router.get("/v1/")
|
|
async def heat_versions(conn: Annotated[Connection, Depends(get_conn)]) -> dict[str, object]:
|
|
from app.openstack.db_docs import require_doc
|
|
|
|
return await require_doc(
|
|
conn, service="heat", resource_type="discovery_version", name="default"
|
|
)
|
|
|
|
|
|
@router.get("/v1/{tenant_id}/stacks")
|
|
async def list_stacks(
|
|
tenant_id: str,
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
from app.openstack.paging import paginate_rows
|
|
|
|
_ = tenant_id
|
|
rows = list(
|
|
await conn.fetch(
|
|
"SELECT * FROM os_stacks WHERE project_id = $1 ORDER BY created_at, id",
|
|
ctx.project_id,
|
|
)
|
|
)
|
|
page, links = paginate_rows(rows, request, id_attr=lambda r: str(r["id"]))
|
|
body: dict[str, object] = {"stacks": [_stack(r) for r in page]}
|
|
if links:
|
|
body["stacks_links"] = links
|
|
return body
|
|
|
|
|
|
@router.get("/v1/{tenant_id}/stacks/detail")
|
|
async def list_stacks_detail(
|
|
tenant_id: str,
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
return await list_stacks(tenant_id, request, conn, ctx)
|
|
|
|
|
|
@router.post("/v1/{tenant_id}/stacks", status_code=201)
|
|
async def create_stack(
|
|
tenant_id: str,
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
from app.openstack.db_docs import fetch_doc
|
|
|
|
_ = tenant_id
|
|
payload = await request.json()
|
|
stack = payload.get("stack") or payload
|
|
defaults = (
|
|
await fetch_doc(conn, service="heat", resource_type="stack_defaults", name="default") or {}
|
|
)
|
|
name = stack.get("stack_name") or stack.get("name") or f"stack-{uuid4().hex[:8]}"
|
|
template = (
|
|
stack.get("template")
|
|
if isinstance(stack.get("template"), dict)
|
|
else defaults.get("template")
|
|
)
|
|
parameters = (
|
|
stack.get("parameters")
|
|
if isinstance(stack.get("parameters"), dict)
|
|
else defaults.get("parameters")
|
|
)
|
|
if not isinstance(template, dict):
|
|
template = {}
|
|
if not isinstance(parameters, dict):
|
|
parameters = {}
|
|
row = await conn.fetchrow(
|
|
"""INSERT INTO os_stacks(id, project_id, stack_name, stack_status, description, template, parameters, outputs)
|
|
VALUES($1,$2,$3,'CREATE_COMPLETE',$4,$5::jsonb,$6::jsonb,'[]'::jsonb) RETURNING *""",
|
|
uuid4(),
|
|
ctx.project_id,
|
|
name,
|
|
stack.get("description") or "",
|
|
json.dumps(template),
|
|
json.dumps(parameters),
|
|
)
|
|
return {"stack": _stack(row)}
|
|
|
|
|
|
@router.get("/v1/{tenant_id}/stacks/{id}")
|
|
async def show_stack_by_id(
|
|
tenant_id: str,
|
|
id: str,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
_ = tenant_id
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM os_stacks WHERE project_id=$1 AND (id::text=$2 OR stack_name=$2) ORDER BY created_at DESC LIMIT 1",
|
|
ctx.project_id,
|
|
id,
|
|
)
|
|
if row is None:
|
|
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
|
|
return {"stack": _stack(row)}
|
|
|
|
|
|
@router.put("/v1/{tenant_id}/stacks/{id}")
|
|
@router.patch("/v1/{tenant_id}/stacks/{id}")
|
|
async def update_stack_by_id(
|
|
tenant_id: str,
|
|
id: str,
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
_ = tenant_id
|
|
payload = await request.json()
|
|
stack = payload.get("stack") or payload
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM os_stacks WHERE project_id=$1 AND (id::text=$2 OR stack_name=$2) ORDER BY created_at DESC LIMIT 1",
|
|
ctx.project_id,
|
|
id,
|
|
)
|
|
if row is None:
|
|
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
|
|
desc = stack.get("description") if "description" in stack else row["description"]
|
|
await conn.execute(
|
|
"UPDATE os_stacks SET description=$1, updated_at=now(), stack_status='UPDATE_COMPLETE' WHERE id=$2",
|
|
desc,
|
|
row["id"],
|
|
)
|
|
row = await conn.fetchrow("SELECT * FROM os_stacks WHERE id=$1", row["id"])
|
|
return {"stack": _stack(row)}
|
|
|
|
|
|
@router.delete("/v1/{tenant_id}/stacks/{id}", status_code=204)
|
|
async def delete_stack_by_id(
|
|
tenant_id: str,
|
|
id: str,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> Response:
|
|
_ = tenant_id
|
|
result = await conn.execute(
|
|
"DELETE FROM os_stacks WHERE project_id=$1 AND (id::text=$2 OR stack_name=$2)",
|
|
ctx.project_id,
|
|
id,
|
|
)
|
|
if result.endswith("0"):
|
|
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.get("/v1/{tenant_id}/stacks/{stack_name}/{stack_id}")
|
|
@router.get("/v1/{tenant_id}/stacks/{stack_name}")
|
|
async def show_stack(
|
|
tenant_id: str,
|
|
stack_name: str,
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
stack_id: str | None = None,
|
|
) -> dict[str, object]:
|
|
_ = tenant_id
|
|
if stack_name == "detail" and stack_id is None:
|
|
from app.openstack.paging import paginate_rows
|
|
|
|
rows = list(
|
|
await conn.fetch(
|
|
"SELECT * FROM os_stacks WHERE project_id = $1 ORDER BY created_at, id",
|
|
ctx.project_id,
|
|
)
|
|
)
|
|
page, links = paginate_rows(rows, request, id_attr=lambda r: str(r["id"]))
|
|
body: dict[str, object] = {"stacks": [_stack(r) for r in page]}
|
|
if links:
|
|
body["stacks_links"] = links
|
|
return body
|
|
if stack_id:
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM os_stacks WHERE project_id=$1 AND id::text=$2",
|
|
ctx.project_id,
|
|
stack_id,
|
|
)
|
|
else:
|
|
row = await conn.fetchrow(
|
|
"SELECT * FROM os_stacks WHERE project_id=$1 AND stack_name=$2 ORDER BY created_at DESC LIMIT 1",
|
|
ctx.project_id,
|
|
stack_name,
|
|
)
|
|
if row is None:
|
|
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
|
|
return {"stack": _stack(row)}
|
|
|
|
|
|
@router.delete("/v1/{tenant_id}/stacks/{stack_name}/{stack_id}", status_code=204)
|
|
async def delete_stack(
|
|
tenant_id: str,
|
|
stack_name: str,
|
|
stack_id: str,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> Response:
|
|
_ = tenant_id, stack_name
|
|
result = await conn.execute(
|
|
"DELETE FROM os_stacks WHERE project_id=$1 AND id::text=$2",
|
|
ctx.project_id,
|
|
stack_id,
|
|
)
|
|
if result.endswith("0"):
|
|
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.get("/v1/{tenant_id}/resource_types")
|
|
async def resource_types(
|
|
tenant_id: str,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
_ctx: Annotated[TokenContext, Depends(require_token)],
|
|
) -> dict[str, object]:
|
|
_ = tenant_id
|
|
from app.openstack.db_docs import require_doc
|
|
|
|
return await require_doc(
|
|
conn, service="heat", resource_type="resource_type_list", name="default"
|
|
)
|