Rename Installation to Instance
Follows the portal: the noun is "instance" everywhere the app says it — UI strings, CLI output, error details, docs and comments. The wire keys (`instance_id`, `instance_token`) and the hub route this calls move with it. An existing cloud.json is adopted rather than refused: without the key alias the dataclass fails to parse, which the caller swallows and reads as "never enrolled" instead of "reconnect". `instance_key` on a node type becomes `target_key`. It means the outside thing a node points at, which is a different sense of the word, and keeping both would put two meanings of "instance" in one codebase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
This commit is contained in:
@@ -51,7 +51,7 @@ def _panel_for(payload: dict[str, Any]) -> panels.PanelDef:
|
||||
effect at once. Or the panel's nonce moved on, which revokes exactly one
|
||||
screen's and leaves the panel, its dashboards and their arrangement alone.
|
||||
|
||||
The nonce is only held against a credential this installation signed. One
|
||||
The nonce is only held against a credential this instance signed. One
|
||||
the portal minted for a remote screen carries none — the portal names the
|
||||
panel and nothing else — and is revoked at the hub instead.
|
||||
"""
|
||||
@@ -182,11 +182,11 @@ def decode_token(token: str, request: Request | None = None) -> dict[str, Any]:
|
||||
guards.
|
||||
|
||||
The last branch is the seam a hosted deployment widens: a portal this
|
||||
installation was enrolled with signs tokens with a key pinned at
|
||||
instance was enrolled with signs tokens with a key pinned at
|
||||
enrolment, and they name the portal account holding them, which resolves
|
||||
to whichever local account was mapped to it — the superuser who enrolled,
|
||||
or a remote user one of them admitted since. With no enrolment the branch
|
||||
raises immediately, so an offline installation pays nothing for the
|
||||
raises immediately, so an offline instance pays nothing for the
|
||||
possibility. A screen that paired through that portal
|
||||
arrives there too, naming a panel — which is why the gate below is applied
|
||||
to whichever branch produced the claims rather than to one of them: where a
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Connecting this installation to a Fluksio portal, and cutting it loose.
|
||||
"""Connecting this instance to a Fluksio portal, and cutting it loose.
|
||||
|
||||
Entirely optional, and superuser-only to change: enrolling grants a remote
|
||||
party the rights of the account that performed it, which is not a decision an
|
||||
ordinary user of this installation gets to make on everyone else's behalf.
|
||||
ordinary user of this instance gets to make on everyone else's behalf.
|
||||
|
||||
Admitting further portal accounts is the same decision made again, so it is
|
||||
guarded the same way. A person let in this way gets an ordinary local account
|
||||
@@ -47,7 +47,7 @@ class EnrollBody(BaseModel):
|
||||
|
||||
@router.get("/status", dependencies=[Depends(get_current_user)])
|
||||
def read_status(request: Request) -> dict[str, Any]:
|
||||
"""Whether this installation is enrolled, and whether the link is up.
|
||||
"""Whether this instance is enrolled, and whether the link is up.
|
||||
|
||||
Readable by any signed-in user: everyone here has a right to know whether
|
||||
the machine they are using can be reached from outside.
|
||||
@@ -61,7 +61,7 @@ def read_status(request: Request) -> dict[str, Any]:
|
||||
"portal_url": config.portal_url if config else None,
|
||||
"issuer": config.issuer if config else None,
|
||||
"portal_account": config.portal_account if config else None,
|
||||
"installation_id": config.installation_id if config else None,
|
||||
"instance_id": config.instance_id if config else None,
|
||||
"last_error": None,
|
||||
"connected_since": None,
|
||||
}
|
||||
@@ -80,7 +80,7 @@ async def enroll(
|
||||
"""Redeem a claim code and start dialling the portal.
|
||||
|
||||
The account performing this is mapped to the portal account that owns the
|
||||
installation, so the owner's portal sessions arrive here as them. Widening
|
||||
instance, so the owner's portal sessions arrive here as them. Widening
|
||||
that to anyone else is a local decision made one person at a time, below —
|
||||
never something the portal can do from its side.
|
||||
|
||||
@@ -121,11 +121,11 @@ def add_remote_user(session: SessionDep, body: RemoteUserBody) -> Any:
|
||||
if config is None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="This installation is not connected to a portal",
|
||||
detail="This instance is not connected to a portal",
|
||||
)
|
||||
try:
|
||||
response = httpx.post(
|
||||
f"{config.portal_url.rstrip('/')}/api/v1/installation-members/",
|
||||
f"{config.portal_url.rstrip('/')}/api/v1/instance-members/",
|
||||
headers={"Authorization": f"Bearer {config.token}"},
|
||||
json={"code": body.code.strip()},
|
||||
timeout=15.0,
|
||||
@@ -140,7 +140,7 @@ def add_remote_user(session: SessionDep, body: RemoteUserBody) -> Any:
|
||||
)
|
||||
if response.status_code == 409:
|
||||
raise HTTPException(
|
||||
status_code=409, detail="That code belongs to this installation's owner"
|
||||
status_code=409, detail="That code belongs to this instance's owner"
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise HTTPException(
|
||||
@@ -183,14 +183,14 @@ def forget_remote_user(portal_sub: str) -> None:
|
||||
|
||||
Best effort on purpose: the local account is what grants access, so it is
|
||||
already over by the time this runs. A portal that cannot be reached keeps a
|
||||
row that opens nothing — the installation refuses the session either way.
|
||||
row that opens nothing — the instance refuses the session either way.
|
||||
"""
|
||||
config = cloud_config.load()
|
||||
if config is None:
|
||||
return
|
||||
try:
|
||||
response = httpx.delete(
|
||||
f"{config.portal_url.rstrip('/')}/api/v1/installation-members/{portal_sub}",
|
||||
f"{config.portal_url.rstrip('/')}/api/v1/instance-members/{portal_sub}",
|
||||
headers={"Authorization": f"Bearer {config.token}"},
|
||||
timeout=15.0,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ shows it on the wall, and somebody with an account types that code into the
|
||||
panels dialog to say which panel the device is. The device polls, collects the
|
||||
credential the approval minted, and never asks again.
|
||||
|
||||
A screen hanging somewhere this installation is not reachable from does the
|
||||
A screen hanging somewhere this instance is not reachable from does the
|
||||
same thing through the portal, which forwards those two calls down the tunnel
|
||||
without a session — a device with no credential is the whole point of them —
|
||||
and mints the credential itself when the approval comes. Which side minted it
|
||||
@@ -81,7 +81,7 @@ class _Pending(BaseModel):
|
||||
#: Self-reported and worth what that is worth.
|
||||
device: str = ""
|
||||
#: Whether it came down the tunnel. A device that reached the portal
|
||||
#: cannot reach this installation, so its credential has to be minted
|
||||
#: cannot reach this instance, so its credential has to be minted
|
||||
#: where it can collect it.
|
||||
remote: bool = False
|
||||
|
||||
@@ -99,7 +99,7 @@ class _PendingStore:
|
||||
A device's poll lands on whichever API worker the proxy picked, and a
|
||||
screen pairing through the portal lands on whichever one holds the tunnel
|
||||
— so a code minted by one worker has to be findable from the next. Redis
|
||||
is where this installation already keeps what must outlive a process.
|
||||
is where this instance already keeps what must outlive a process.
|
||||
|
||||
Without one there is one process by definition (the pip install, and the
|
||||
tests), and a dictionary is the same thing for it.
|
||||
@@ -224,8 +224,8 @@ def _describe(request: Request) -> str:
|
||||
def _mint_at_hub(panel_id: str) -> str:
|
||||
"""Ask the portal for this panel's credential.
|
||||
|
||||
A device that arrived through the portal cannot reach this installation, so
|
||||
a token this installation signed would be one it could never present: the
|
||||
A device that arrived through the portal cannot reach this instance, so
|
||||
a token this instance signed would be one it could never present: the
|
||||
portal verifies what crosses its tunnel, and it verifies against its own
|
||||
key. It mints, we say which panel — and the scope check here decides the
|
||||
rest, on this call and on every later one.
|
||||
@@ -234,7 +234,7 @@ def _mint_at_hub(panel_id: str) -> str:
|
||||
if config is None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="That device came through a portal this installation is no "
|
||||
detail="That device came through a portal this instance is no "
|
||||
"longer enrolled with",
|
||||
)
|
||||
try:
|
||||
@@ -264,14 +264,14 @@ class PanelsPublic(BaseModel):
|
||||
reliable answer to it: an admin working through the portal is on the
|
||||
portal's origin, and this one is for a screen on this network.
|
||||
|
||||
An installation enrolled with a portal has a second address, built by the
|
||||
An instance enrolled with a portal has a second address, built by the
|
||||
dialog from what ``/cloud/status`` reports rather than from here — a panel
|
||||
is not the thing that knows whether remote access is on.
|
||||
"""
|
||||
|
||||
panels: list[PanelDef] = Field(default_factory=list)
|
||||
#: Whatever this installation was told it is reachable at. The same setting
|
||||
#: the password-reset links are built from, so an installation that has it
|
||||
#: Whatever this instance was told it is reachable at. The same setting
|
||||
#: the password-reset links are built from, so an instance that has it
|
||||
#: wrong has it wrong in both places.
|
||||
frontend_host: str = ""
|
||||
|
||||
@@ -310,7 +310,7 @@ async def save_panels(body: PanelsConfig) -> Any:
|
||||
|
||||
def _save() -> None:
|
||||
# Read and write under one lock. The nonce belongs to this
|
||||
# installation, not to whoever is writing the panels back: a client
|
||||
# instance, not to whoever is writing the panels back: a client
|
||||
# holding an older copy must not be able to undo a revocation by
|
||||
# saving an arrangement — and reading it in a separate step from
|
||||
# writing it is exactly how an unpair in between was undone.
|
||||
@@ -335,7 +335,7 @@ def start_pairing(request: Request) -> Any:
|
||||
All this hands out is a code that means nothing until somebody with an
|
||||
account approves it, so the worst an unwelcome caller achieves is an entry
|
||||
that expires ten minutes later. Reachable from the internet when this
|
||||
installation is enrolled with a portal, which is what the cap and the
|
||||
instance is enrolled with a portal, which is what the cap and the
|
||||
portal's own per-address limits are between.
|
||||
"""
|
||||
if _pending.count() >= MAX_PENDING:
|
||||
@@ -412,8 +412,8 @@ def approve_pairing(panel_id: str, body: PairRequest, current_user: CurrentUser)
|
||||
|
||||
A credential minted here names the approver, so what the panel does stays
|
||||
attributable to a person rather than to nobody. One minted by the portal —
|
||||
for a device that reached this installation only through it — names the
|
||||
account this installation was enrolled with instead, since that is the one
|
||||
for a device that reached this instance only through it — names the
|
||||
account this instance was enrolled with instead, since that is the one
|
||||
every portal-borne request already acts as.
|
||||
"""
|
||||
panel = find(panel_id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""One index of everything in this installation worth jumping to by name."""
|
||||
"""One index of everything in this instance worth jumping to by name."""
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -20,7 +20,7 @@ from fluksio.flow.secrets import get_secrets
|
||||
from fluksio.flow.store import FlowNotFound
|
||||
|
||||
# A wall panel never reaches this route: ``deps._panel_may`` is a whitelist that
|
||||
# ends in a 403, and a whole-installation index is the opposite of what a screen
|
||||
# ends in a 403, and a whole-instance index is the opposite of what a screen
|
||||
# on a wall is allowed to read.
|
||||
router = APIRouter(
|
||||
prefix="/search", tags=["search"], dependencies=[Depends(get_current_user)]
|
||||
@@ -144,7 +144,7 @@ async def read_search_index(
|
||||
"""Everything searchable, for the client to match against as it is typed.
|
||||
|
||||
The whole index rather than a query: it is a few hundred short rows for an
|
||||
installation of any ordinary size, so one fetch when the panel opens beats a
|
||||
instance of any ordinary size, so one fetch when the panel opens beats a
|
||||
round trip per keystroke — and the client already has a matcher.
|
||||
|
||||
Secrets are named only to a superuser, which is who ``/secrets`` answers to.
|
||||
|
||||
@@ -13,7 +13,7 @@ from fluksio.api.deps import get_current_active_superuser
|
||||
from fluksio.flow.secrets import SecretNotFound, get_secrets
|
||||
from fluksio.models import Message
|
||||
|
||||
# Superuser, not merely signed in. The names alone say what this installation
|
||||
# Superuser, not merely signed in. The names alone say what this instance
|
||||
# talks to, and `PUT /{name}` takes any name — so an ordinary account could
|
||||
# overwrite the credential a flow authenticates with. `/search` already gates
|
||||
# secret names this way and said so; this router was the half that did not.
|
||||
|
||||
Reference in New Issue
Block a user