Give connectors a store that survives restarts, and a way to clear it
`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.
This commit is contained in:
@@ -5,6 +5,7 @@ from fluksio.api.routes import (
|
||||
alerts,
|
||||
artifacts,
|
||||
cloud,
|
||||
connectors,
|
||||
dashboards,
|
||||
flavors,
|
||||
flows,
|
||||
@@ -31,6 +32,7 @@ api_router.include_router(flavors.router)
|
||||
api_router.include_router(flows.router)
|
||||
api_router.include_router(flows.ws_router)
|
||||
api_router.include_router(secrets.router)
|
||||
api_router.include_router(connectors.router)
|
||||
api_router.include_router(alerts.router)
|
||||
api_router.include_router(dashboards.router)
|
||||
api_router.include_router(panels.router)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""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}")
|
||||
Reference in New Issue
Block a user