`ConnectorNode.store` keeps short strings per connector package in the
engine's database (`connector_state`, migration f8c3e5a91d27), so the nodes of
one package share them and `fluksio serve` needs nothing new: get, set and
delete, at most 64 kB a value and 256 keys a package. A superuser lists the
keys (never the values) and clears them through `/connectors/{node_type}/state`,
or under Stored state in a connector node's panel. Documented in the connector
contract.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""What a connector keeps across restarts, for a superuser to see and reset.
|
|
|
|
A connector writes these itself (``ConnectorNode.store``). This is the way back
|
|
when one of them is wrong: a Hue bridge replaced, so the certificate pinned for
|
|
it no longer matches. Values are never handed out, since a connector may keep a
|
|
token here; the keys say enough to decide what to clear.
|
|
|
|
A connector is named by any node type it installs, as the canvas knows it, and
|
|
its keys are shared by every node type of the same package.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from fluksio.api.deps import get_current_active_superuser
|
|
from fluksio.flow.connector import ConnectorNode
|
|
from fluksio.flow.connector_store import KEY, ConnectorStore, namespace_of
|
|
from fluksio.flow.controller import NODE_TYPES
|
|
from fluksio.models import Message
|
|
|
|
router = APIRouter(
|
|
prefix="/connectors",
|
|
tags=["connectors"],
|
|
dependencies=[Depends(get_current_active_superuser)],
|
|
)
|
|
|
|
|
|
class StoredKey(BaseModel):
|
|
key: str
|
|
#: Bytes, so a superuser can tell a fingerprint from a blob.
|
|
size: int
|
|
updated_at: datetime
|
|
|
|
|
|
class ConnectorStatePublic(BaseModel):
|
|
#: The package the keys belong to.
|
|
connector: str
|
|
data: list[StoredKey]
|
|
count: int
|
|
|
|
|
|
def _store(node_type: str) -> ConnectorStore:
|
|
"""The store of the connector that installed *node_type*, or a 404."""
|
|
known = NODE_TYPES.get(node_type)
|
|
cls = known.cls if known is not None else None
|
|
if not (isinstance(cls, type) and issubclass(cls, ConnectorNode)):
|
|
raise HTTPException(status_code=404, detail=f"No connector named '{node_type}'")
|
|
return ConnectorStore(namespace_of(cls))
|
|
|
|
|
|
@router.get("/{node_type}/state", response_model=ConnectorStatePublic)
|
|
def read_connector_state(node_type: str) -> Any:
|
|
"""The keys this connector keeps, without their values."""
|
|
store = _store(node_type)
|
|
keys = [
|
|
StoredKey(key=row.key, size=len(row.value.encode()), updated_at=row.updated_at)
|
|
for row in store.entries()
|
|
]
|
|
return ConnectorStatePublic(connector=store.connector, data=keys, count=len(keys))
|
|
|
|
|
|
@router.delete("/{node_type}/state", response_model=Message)
|
|
def clear_connector_state(node_type: str) -> Any:
|
|
"""Forget everything this connector keeps. It starts over on next use."""
|
|
store = _store(node_type)
|
|
cleared = store.clear()
|
|
return Message(message=f"Cleared {cleared} key(s) of {store.connector}")
|
|
|
|
|
|
@router.delete("/{node_type}/state/{key}", response_model=Message)
|
|
def delete_connector_state(node_type: str, key: str) -> Any:
|
|
"""Forget one key, such as the certificate pinned for one bridge."""
|
|
store = _store(node_type)
|
|
if not KEY.fullmatch(key) or not store.delete(key):
|
|
raise HTTPException(status_code=404, detail=f"No key '{key}' stored")
|
|
return Message(message=f"Deleted '{key}' of {store.connector}")
|