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:
@@ -9,7 +9,7 @@ from fluksio.core.db import seed_flavors
|
||||
from fluksio.models import Flavor
|
||||
|
||||
|
||||
def test_an_installation_starts_with_sizes_to_pick_from(
|
||||
def test_an_instance_starts_with_sizes_to_pick_from(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
response = client.get(
|
||||
@@ -20,7 +20,7 @@ def test_an_installation_starts_with_sizes_to_pick_from(
|
||||
assert {"small", "medium", "large", "gpu-small"} <= names
|
||||
|
||||
|
||||
def test_seeding_leaves_an_installation_that_has_its_own_alone(db: Session) -> None:
|
||||
def test_seeding_leaves_an_instance_that_has_its_own_alone(db: Session) -> None:
|
||||
"""Re-adding a size somebody deliberately removed is an argument to avoid."""
|
||||
before = {row.name for row in db.exec(select(Flavor)).all()}
|
||||
seed_flavors(db)
|
||||
|
||||
@@ -544,7 +544,7 @@ def test_removing_the_panel_revokes_its_credential(
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# A screen that reached the portal but not this installation
|
||||
# A screen that reached the portal but not this instance
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -592,7 +592,7 @@ def test_a_remote_device_is_paired_at_the_portal(
|
||||
) -> None:
|
||||
"""A device that arrived through the tunnel gets the portal's credential.
|
||||
|
||||
It could never present one this installation signed: the portal verifies
|
||||
It could never present one this instance signed: the portal verifies
|
||||
what crosses it, and it verifies against its own key.
|
||||
"""
|
||||
import httpx
|
||||
@@ -627,7 +627,7 @@ def test_a_remote_device_is_paired_at_the_portal(
|
||||
assert approved.status_code == 200, approved.text
|
||||
|
||||
assert calls[0]["url"].endswith("/api/v1/panel-tokens/") # type: ignore[union-attr]
|
||||
assert calls[0]["headers"]["Authorization"] == "Bearer installation-token" # type: ignore[index]
|
||||
assert calls[0]["headers"]["Authorization"] == "Bearer instance-token" # type: ignore[index]
|
||||
assert calls[0]["json"] == {"panel": "hallway"} # type: ignore[index]
|
||||
|
||||
collected = client.get(
|
||||
|
||||
@@ -25,7 +25,7 @@ from fluksio.models import (
|
||||
RunNode,
|
||||
User,
|
||||
)
|
||||
from tests.utils.portal import INSTALLATION_ID, ISSUER, jwks
|
||||
from tests.utils.portal import INSTANCE_ID, ISSUER, jwks
|
||||
from tests.utils.user import authentication_token_from_email
|
||||
from tests.utils.utils import get_superuser_token_headers
|
||||
|
||||
@@ -129,7 +129,7 @@ def enrolled(
|
||||
portal_key: rsa.RSAPrivateKey,
|
||||
db: Session,
|
||||
) -> Any:
|
||||
"""Enrol this installation with a fake portal, then undo it."""
|
||||
"""Enrol this instance with a fake portal, then undo it."""
|
||||
local_user = db.exec(
|
||||
select(User).where(User.email == settings.FIRST_SUPERUSER)
|
||||
).one()
|
||||
@@ -141,8 +141,8 @@ def enrolled(
|
||||
cloud_config.CloudConfig(
|
||||
portal_url=ISSUER,
|
||||
ws_url=f"{ISSUER}/api/v1/tunnel/attach",
|
||||
installation_id=INSTALLATION_ID,
|
||||
token="installation-token",
|
||||
instance_id=INSTANCE_ID,
|
||||
token="instance-token",
|
||||
issuer=ISSUER,
|
||||
jwks=jwks(portal_key),
|
||||
local_user_id=str(local_user.id),
|
||||
@@ -151,7 +151,7 @@ def enrolled(
|
||||
)
|
||||
)
|
||||
# Enrolment also maps the enrolling account to the portal identity that
|
||||
# owns the installation; without it a portal session resolves to nobody.
|
||||
# owns the instance; without it a portal session resolves to nobody.
|
||||
local_user.portal_sub = "portal-user-1"
|
||||
db.add(local_user)
|
||||
db.commit()
|
||||
|
||||
@@ -78,7 +78,7 @@ def test_an_edge_carries_the_qualified_message(controller: FlowController):
|
||||
def test_a_credential_never_reaches_the_key():
|
||||
# Stored params, so a secret is still a reference. Neither its name nor its
|
||||
# value belongs in something the browser gets to see.
|
||||
key = MqttNode.instance_key({**BROKER, "password": {"$secret": "broker_pw"}})
|
||||
key = MqttNode.target_key({**BROKER, "password": {"$secret": "broker_pw"}})
|
||||
|
||||
assert key == "mosquitto:1883/sensors/temp"
|
||||
|
||||
|
||||
@@ -423,7 +423,7 @@ def test_an_unbound_setting_is_just_its_value():
|
||||
|
||||
No message means nothing to type-check and nothing for a panel to be
|
||||
entitled to — and a name this build does not know is left alone rather
|
||||
than refused, so an older installation reads a newer document.
|
||||
than refused, so an older instance reads a newer document.
|
||||
"""
|
||||
defn = DashboardDef(
|
||||
name="house",
|
||||
@@ -523,7 +523,7 @@ def test_a_chart_of_runs_must_say_which_runs_and_which_metric():
|
||||
|
||||
|
||||
def test_a_document_written_as_pages_is_read_as_one_grid():
|
||||
"""Stored dashboards live in each installation's repository.
|
||||
"""Stored dashboards live in each instance's repository.
|
||||
|
||||
So the old shape is normalised on the way in rather than migrated, and a
|
||||
placed second section keeps its arrangement instead of piling onto the
|
||||
|
||||
@@ -192,7 +192,7 @@ def test_a_node_waiting_for_a_machine_asks_for_one_once(monkeypatch):
|
||||
assert ran.is_set()
|
||||
|
||||
|
||||
def test_an_installation_with_nowhere_to_start_one_has_no_file(tmp_path):
|
||||
def test_an_instance_with_nowhere_to_start_one_has_no_file(tmp_path):
|
||||
assert load_provisioners(tmp_path / "provisioners.json") == []
|
||||
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ def lifecycle(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
||||
def test_rebuilding_one_flow_leaves_another_flows_node_running(
|
||||
tmp_path: Path, lifecycle: list[str]
|
||||
):
|
||||
"""The whole point: reconnecting one flow's nodes, not the installation's."""
|
||||
"""The whole point: reconnecting one flow's nodes, not the instance's."""
|
||||
store = FlowStore(tmp_path / "flows")
|
||||
for name in ("a", "b"):
|
||||
store.write_flow(FlowDef(name=name, nodes=[NodeDef(id="io", type="lifecycle")]))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""What the flow-logic nodes actually do.
|
||||
|
||||
These are the shapes a Node-RED installation is mostly made of, so their
|
||||
These are the shapes a Node-RED instance is mostly made of, so their
|
||||
behaviour is worth pinning rather than just their construction.
|
||||
"""
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from fluksio.flow import webpush
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def store(tmp_path, monkeypatch):
|
||||
"""A store per test, so nothing writes into the real installation."""
|
||||
"""A store per test, so nothing writes into the real instance."""
|
||||
monkeypatch.setattr(settings, "WEBPUSH_FILE", tmp_path / "webpush.json")
|
||||
return tmp_path / "webpush.json"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Which installation a command is for, and where its token lives.
|
||||
"""Which instance a command is for, and where its token lives.
|
||||
|
||||
A repository with its own venv gets its own engine, so "which one" is a fact
|
||||
about the working directory rather than about the machine.
|
||||
@@ -14,14 +14,14 @@ from fluksio.sdk import client
|
||||
|
||||
@pytest.fixture
|
||||
def elsewhere(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
"""A working directory with no installation above it."""
|
||||
"""A working directory with no instance above it."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
|
||||
monkeypatch.setattr(client, "GLOBAL_DATA_DIR", tmp_path / "home" / ".fluksio")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_a_project_local_installation_is_found_from_below(elsewhere: Path):
|
||||
def test_a_project_local_instance_is_found_from_below(elsewhere: Path):
|
||||
(elsewhere / ".fluksio").mkdir()
|
||||
deep = elsewhere / "src" / "pkg" / "sub"
|
||||
deep.mkdir(parents=True)
|
||||
@@ -31,7 +31,7 @@ def test_a_project_local_installation_is_found_from_below(elsewhere: Path):
|
||||
|
||||
|
||||
def test_the_nearest_one_wins(elsewhere: Path):
|
||||
"""An installation inside another belongs to the directory it is in."""
|
||||
"""An instance inside another belongs to the directory it is in."""
|
||||
(elsewhere / ".fluksio").mkdir()
|
||||
inner = elsewhere / "inner"
|
||||
(inner / ".fluksio").mkdir(parents=True)
|
||||
@@ -63,7 +63,7 @@ def test_the_credential_beside_the_data_is_the_one_used(elsewhere: Path):
|
||||
assert client._stored()["token"] == "local"
|
||||
|
||||
|
||||
def test_a_login_from_before_installations_were_local_still_works(elsewhere: Path):
|
||||
def test_a_login_from_before_instances_were_local_still_works(elsewhere: Path):
|
||||
"""`fluksio login` wrote to XDG once; that must not stop answering."""
|
||||
legacy = client._legacy_config_path()
|
||||
legacy.parent.mkdir(parents=True)
|
||||
|
||||
@@ -106,7 +106,7 @@ def test_a_sweeps_param_spelling_is_refused_by_name() -> None:
|
||||
_params(definition, ["--lr", "fast"])
|
||||
|
||||
|
||||
def test_serve_uses_the_installation_the_directory_belongs_to(
|
||||
def test_serve_uses_the_instance_the_directory_belongs_to(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""A repository with its own venv gets its own engine, not the machine's."""
|
||||
@@ -713,7 +713,7 @@ def test_who_holds_the_port_is_told_apart_by_the_token() -> None:
|
||||
|
||||
The token is signed with this directory's secret key, so an engine that
|
||||
accepts it is one reading this directory's database. Another
|
||||
installation's Fluksio answers the health check and refuses it.
|
||||
instance's Fluksio answers the health check and refuses it.
|
||||
"""
|
||||
import httpx
|
||||
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
"""Remote access: off by default, and only ever as much as somebody granted.
|
||||
|
||||
The property worth pinning down is the one everything else rests on — a
|
||||
portal's token is worth nothing here until somebody at this installation
|
||||
portal's token is worth nothing here until somebody at this instance
|
||||
enrolled it, and even then it grants exactly the rights of the local account
|
||||
the portal identity holding it was mapped to. An identity nobody mapped gets
|
||||
nothing, which is what makes deleting that local account a revocation.
|
||||
@@ -31,7 +31,7 @@ from tests.utils.portal import ISSUER, portal_token
|
||||
def test_portal_token_is_refused_when_not_enrolled(
|
||||
portal_key: rsa.RSAPrivateKey, tmp_path_factory: pytest.TempPathFactory
|
||||
) -> None:
|
||||
"""An installation nobody connected trusts no portal at all."""
|
||||
"""An instance nobody connected trusts no portal at all."""
|
||||
original = settings.CLOUD_CONFIG_FILE
|
||||
settings.CLOUD_CONFIG_FILE = tmp_path_factory.mktemp("empty") / "cloud.json"
|
||||
try:
|
||||
@@ -55,11 +55,11 @@ def test_portal_token_resolves_by_portal_identity(
|
||||
assert user_from_token(db, portal_token(portal_key, subject="nobody")) is None
|
||||
|
||||
|
||||
def test_token_for_another_installation_is_refused(
|
||||
def test_token_for_another_instance_is_refused(
|
||||
enrolled: User, # noqa: ARG001 (fixture installs the enrolment)
|
||||
portal_key: rsa.RSAPrivateKey,
|
||||
) -> None:
|
||||
"""The audience is this installation's id, so someone else's is worthless."""
|
||||
"""The audience is this instance's id, so someone else's is worthless."""
|
||||
with pytest.raises(jwt.exceptions.InvalidTokenError):
|
||||
decode_token(portal_token(portal_key, audience=str(uuid.uuid4())))
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_token_for_another_installation_is_refused(
|
||||
def test_token_from_an_unpinned_key_is_refused(
|
||||
enrolled: User, # noqa: ARG001 (fixture installs the enrolment)
|
||||
) -> None:
|
||||
"""A different portal, or a hijacked one, cannot sign for this installation."""
|
||||
"""A different portal, or a hijacked one, cannot sign for this instance."""
|
||||
impostor = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
with pytest.raises(jwt.exceptions.InvalidTokenError):
|
||||
decode_token(portal_token(impostor))
|
||||
@@ -104,7 +104,7 @@ def test_status_reports_not_enrolled(
|
||||
def test_enrolling_needs_a_superuser(
|
||||
client: TestClient, normal_user_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
"""Remote access is an installation-wide grant, not a personal setting."""
|
||||
"""Remote access is an instance-wide grant, not a personal setting."""
|
||||
response = client.post(
|
||||
f"{settings.API_V1_STR}/cloud/enroll",
|
||||
headers=normal_user_token_headers,
|
||||
@@ -122,7 +122,7 @@ def test_a_panel_scoped_portal_token_reaches_only_its_panel(
|
||||
"""A screen paired through the portal is bounded here, not there.
|
||||
|
||||
The portal names the panel; everything about what that means is this
|
||||
installation's, which is the whole reason it may mint one at all.
|
||||
instance's, which is the whole reason it may mint one at all.
|
||||
"""
|
||||
write_config(
|
||||
PanelsConfig(
|
||||
@@ -241,7 +241,7 @@ def test_adding_a_remote_user_maps_and_revokes(
|
||||
f"{settings.API_V1_STR}/users/{body['id']}", headers=superuser_token_headers
|
||||
)
|
||||
assert removed.status_code == 200, removed.text
|
||||
assert delete.call_args.args[0].endswith("/installation-members/portal-user-9")
|
||||
assert delete.call_args.args[0].endswith("/instance-members/portal-user-9")
|
||||
db.expire_all()
|
||||
assert user_from_token(db, theirs) is None
|
||||
|
||||
@@ -251,7 +251,7 @@ def test_adding_a_remote_user_needs_a_superuser(
|
||||
enrolled: User, # noqa: ARG001 (fixture installs the enrolment)
|
||||
normal_user_token_headers: dict[str, str],
|
||||
) -> None:
|
||||
"""Widening who can reach this installation stays a superuser's decision."""
|
||||
"""Widening who can reach this instance stays a superuser's decision."""
|
||||
response = client.post(
|
||||
f"{settings.API_V1_STR}/cloud/users",
|
||||
headers=normal_user_token_headers,
|
||||
@@ -272,8 +272,8 @@ def test_enrolling_against_a_portal_without_an_owner_is_refused(
|
||||
status_code=200,
|
||||
json=Mock(
|
||||
return_value={
|
||||
"installation_id": str(uuid.uuid4()),
|
||||
"installation_token": "t",
|
||||
"instance_id": str(uuid.uuid4()),
|
||||
"instance_token": "t",
|
||||
"ws_url": f"{ISSUER}/api/v1/tunnel/attach",
|
||||
"issuer": ISSUER,
|
||||
"jwks": {"keys": []},
|
||||
|
||||
@@ -14,7 +14,7 @@ import jwt
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
INSTALLATION_ID = "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f"
|
||||
INSTANCE_ID = "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f"
|
||||
ISSUER = "https://hub.example.test"
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ def portal_token(
|
||||
key: rsa.RSAPrivateKey,
|
||||
*,
|
||||
subject: str = "portal-user-1",
|
||||
audience: str = INSTALLATION_ID,
|
||||
audience: str = INSTANCE_ID,
|
||||
issuer: str = ISSUER,
|
||||
scope: str = "proxy",
|
||||
) -> str:
|
||||
|
||||
Reference in New Issue
Block a user