Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions atlas/atlas/api/provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,35 +104,6 @@ def create_vm(
}


@frappe.whitelist()
def regenerate_vm_login(name: str) -> dict:
"""Re-mint a bench VM's one-click login URL and return its (VM-shaped) mirror row.

Central calls this on Open when the Asset's stored URL has expired or never
arrived (the admin JWT lasts 5 minutes, so a login is almost always a fresh mint).
Central knows the VM only — it mirrors VMs — but the login URL lives on the front
door that owns the VM (a `Pilot` for a bench, a `Site` for a self-serve site), not
the pure-microVM `Virtual Machine`. So this resolves the VM to its front door,
re-mints in the guest via its `regenerate_login_url` (re-stamps + commits), then
returns the VM-shaped payload — always the Asset shape Central re-reads, whichever
aggregate backs the VM (the Site's own regenerate returns a site-shaped mirror; the
Asset caller needs the VM shape keyed by VM id, so we re-derive it here).

Raises if the VM has no front door (a plain proxy/operator VM has no login to
regenerate) — Central only ever calls this for a bench/site Asset.
"""
from atlas.atlas.central_report import _vm_payload
from atlas.atlas.front_door import front_door_for_vm

front_door = front_door_for_vm(name)
if front_door is None:
frappe.throw(f"No bench or site front door backs VM {name}.")
front_door.regenerate_login_url()
# Re-read the VM: its front door just committed the fresh login_url, and _vm_payload
# reads the handoff back through that front door — the VM-shaped Asset mirror row.
return _vm_payload(frappe.get_doc("Virtual Machine", name))


@frappe.whitelist()
def capacity() -> dict:
"""What can this region provision right now? — Central's pre-create check.
Expand Down
4 changes: 1 addition & 3 deletions atlas/atlas/api/satellite.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ def list_virtual_machines(modified_after: str | None = None) -> list[dict]:
oldest-change first so a paging caller can advance its watermark."""
frappe.only_for("System Manager")
filters = {"modified": (">", modified_after)} if modified_after else {}
names = frappe.get_all(
"Virtual Machine", filters=filters, pluck="name", order_by="modified asc"
)
names = frappe.get_all("Virtual Machine", filters=filters, pluck="name", order_by="modified asc")
return [_vm_payload(frappe.get_doc("Virtual Machine", n)) for n in names]


Expand Down
4 changes: 2 additions & 2 deletions atlas/atlas/api/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def create_site(
subdomain: str,
pilot_credential_id: str | None = None,
central_endpoint: str | None = None,
central_auth_token: str | None = None,
bootstrap_token: str | None = None,
) -> dict:
"""Provision a self-serve site for a Central team and return its mirror row.

Expand All @@ -54,7 +54,7 @@ def create_site(
site = frappe.get_doc({"doctype": "Site", "subdomain": subdomain, "tenant": tenant})
site.flags.pilot_credential_id = pilot_credential_id
site.flags.central_endpoint = central_endpoint
site.flags.central_auth_token = central_auth_token
site.flags.bootstrap_token = bootstrap_token
site.insert(ignore_permissions=True)

return _mirror(site)
Expand Down
30 changes: 3 additions & 27 deletions atlas/atlas/api/test_provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,30 +148,6 @@ def test_bad_label_is_rejected(self) -> None:
self._create("Not.A.Label")


class TestRegenerateVMLogin(IntegrationTestCase):
"""Central's Open path re-mints an expired bench/site login by VM name. The login
URL lives on the front door (Pilot OR Site — the VM is a pure microVM), so the
endpoint resolves the VM to its front door, re-mints, and returns the VM-shaped
Asset payload — a plain VM with no front door is refused."""

def test_remints_via_front_door_and_returns_vm_payload(self) -> None:
"""A VM backed by a front door (Pilot or Site): re-mint via it, then return the
freshly re-read VM-shaped Asset payload (NOT the front door's own return shape —
a Site's regenerate returns a site-shaped mirror, so we re-derive the VM shape)."""
front_door = MagicMock()
vm_payload = {"name": "vm-1", "login_url": "https://acme.blr1.frappe.dev/app?sid=fresh"}
with (
patch("atlas.atlas.front_door.front_door_for_vm", return_value=front_door) as resolve,
patch("atlas.atlas.central_report._vm_payload", return_value=vm_payload),
patch.object(provision_api.frappe, "get_doc", return_value=MagicMock(name="vm-1")),
):
result = provision_api.regenerate_vm_login("vm-1")
resolve.assert_called_once_with("vm-1")
front_door.regenerate_login_url.assert_called_once_with()
self.assertEqual(result, vm_payload)

def test_vm_without_front_door_is_refused(self) -> None:
"""A plain proxy/operator VM has no bench/site login to regenerate."""
with patch("atlas.atlas.front_door.front_door_for_vm", return_value=None):
with self.assertRaises(frappe.ValidationError):
provision_api.regenerate_vm_login("vm-plain")
# Bench-open no longer re-mints a login via Atlas: Central mints the SID itself (RS256,
# verified by the bench against the JWKS), so `provision.regenerate_vm_login` was retired.
# Site-open still re-mints via the Site front door (`regenerate_site_login`), covered elsewhere.
15 changes: 8 additions & 7 deletions atlas/atlas/deploy_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def deploy_site(
virtual_machine: str,
site_name: str,
central_endpoint: str | None = None,
central_auth_token: str | None = None,
bootstrap_token: str | None = None,
mode: str | None = None,
admin_domain: str | None = None,
) -> dict | None:
Expand Down Expand Up @@ -184,13 +184,14 @@ def _trace(message: str, since: float | None = None) -> None:
# --warm-vm-uuid.
if vm.warm_snapshot:
command += substitute(" --warm-vm-uuid {}", (vm.name,))
# Central handoff: the pilot's bench-level callback endpoint + token, threaded from
# create_site (never stored on the Site). deploy-site.py writes them into the
# bench's bench.toml so pilot→Central calls authenticate.
if central_endpoint and central_auth_token:
# Central handoff: the endpoint + a single-use bootstrap token, threaded from
# create_site (never stored on the Site). deploy-site.py runs `bench enroll` with
# them, so the pilot exchanges the token for its own long-lived credential — the
# durable secret is never injected here.
if central_endpoint and bootstrap_token:
command += substitute(
" --central-endpoint {} --central-auth-token {}",
(central_endpoint, central_auth_token),
" --central-endpoint {} --bootstrap-token {}",
(central_endpoint, bootstrap_token),
)
_trace(
f"running deploy-site.py in guest ({'warm' if vm.warm_snapshot else 'cold'}, mode={build_mode}) …"
Expand Down
15 changes: 10 additions & 5 deletions atlas/atlas/dns/test_powerdns.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ def test_certbot_args_use_powerdns_credentials_file(self) -> None:
self.assertTrue(path.endswith("/.atlas/certbot/blr1.frappe.dev/powerdns.ini"))

def test_credential_env_carries_powerdns_settings(self) -> None:
env = _provider(api_url="https://pdns.example.test/", api_key="shh", server_id="primary").credential_env()
env = _provider(
api_url="https://pdns.example.test/", api_key="shh", server_id="primary"
).credential_env()
self.assertEqual(env["POWERDNS_API_URL"], "https://pdns.example.test")
self.assertEqual(env["POWERDNS_API_KEY"], "shh")
self.assertEqual(env["POWERDNS_SERVER_ID"], "primary")

def test_authenticate_reads_server_endpoint(self) -> None:
provider = _provider(server_id="primary")
with patch.object(provider, "_request", return_value={"id": "primary", "version": "4.9.0"}) as request:
with patch.object(
provider, "_request", return_value={"id": "primary", "version": "4.9.0"}
) as request:
result = provider.authenticate()
self.assertTrue(result.ok)
self.assertEqual(result.account_label, "primary (4.9.0)")
Expand Down Expand Up @@ -76,9 +80,10 @@ def fake_request(method, path, **kwargs):

def test_upsert_wildcard_skips_empty_family(self) -> None:
provider = _provider()
with patch.object(provider, "_zone_id", return_value="x.frappe.dev."), patch.object(
provider, "_request", return_value={}
) as request:
with (
patch.object(provider, "_zone_id", return_value="x.frappe.dev."),
patch.object(provider, "_request", return_value={}) as request,
):
records = provider.upsert_wildcard(
"atlas1.x.frappe.dev", WildcardTargets(ipv4=["1.2.3.4"], ipv6=[])
)
Expand Down
12 changes: 5 additions & 7 deletions atlas/atlas/doctype/site/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def after_insert(self) -> None:
# the backing VM + the bench's bench.toml. Flags are set by create_site.
pilot_credential_id=self.flags.get("pilot_credential_id"),
central_endpoint=self.flags.get("central_endpoint"),
central_auth_token=self.flags.get("central_auth_token"),
bootstrap_token=self.flags.get("bootstrap_token"),
)

# ----- validation -----------------------------------------------------
Expand Down Expand Up @@ -301,7 +301,7 @@ def auto_provision(
site_name: str,
pilot_credential_id: str | None = None,
central_endpoint: str | None = None,
central_auth_token: str | None = None,
bootstrap_token: str | None = None,
) -> None:
"""Background-job entrypoint (enqueued by after_insert). Drives the whole
create_site→live-site flow for one Site:
Expand Down Expand Up @@ -385,7 +385,7 @@ def auto_provision(
pilot_label = pilot_subdomain_for(site.subdomain)
admin_domain = f"{pilot_label}.{active_root_domain().domain}"
clock.stage("deploy site in guest (wait_for_ssh + run deploy-site.py)")
result = _deploy_site(site, vm_name, central_endpoint, central_auth_token, admin_domain)
result = _deploy_site(site, vm_name, central_endpoint, bootstrap_token, admin_domain)
# The tenant handoff is the one-click login URL `deploy-site.py` minted
# (`bench browse --sid`, a real 24h session) — NOT a password; the baked
# Administrator password is a long random secret generated at bake time and
Expand Down Expand Up @@ -554,7 +554,7 @@ def _deploy_site(
site,
vm_name: str,
central_endpoint: str | None = None,
central_auth_token: str | None = None,
bootstrap_token: str | None = None,
admin_domain: str | None = None,
) -> dict:
"""Run deploy-site.py in the guest: rename the baked `site.local` dir to the FQDN
Expand All @@ -577,9 +577,7 @@ def _deploy_site(
vm = frappe.get_doc("Virtual Machine", vm_name)
if is_fake_server(vm.server):
return {"site": site.name, "serving": True, "login_url": f"https://{site.name}/app?sid=fake-sid"}
return (
deploy_site(vm_name, site.name, central_endpoint, central_auth_token, admin_domain=admin_domain) or {}
)
return deploy_site(vm_name, site.name, central_endpoint, bootstrap_token, admin_domain=admin_domain) or {}


def _regenerate_login(site, vm_name: str) -> dict:
Expand Down
16 changes: 7 additions & 9 deletions atlas/atlas/test_deploy_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,10 +638,10 @@ def test_site_main_skips_admin_domain_when_not_given(self) -> None:
guest.main()
m_admin.assert_not_called()

def test_site_main_writes_central_config_after_admin_domain(self) -> None:
"""Ordering invariant: `set-central-config` reads `[admin].domain` out of
bench.toml to write site_config.json, so it MUST run after the admin domain is in
place — never before, or it captures the `admin.localhost` placeholder."""
def test_site_main_enrols_after_admin_domain(self) -> None:
"""Ordering invariant: `bench enroll` runs after the admin domain is in place, so the
pilot's Central config is written to a bench.toml that already carries the real
`[admin].domain` — never the `admin.localhost` placeholder."""
guest = self.guest
order = []
with (
Expand All @@ -656,9 +656,7 @@ def test_site_main_writes_central_config_after_admin_domain(self) -> None:
patch.object(
guest,
"_bench",
side_effect=lambda *a, **k: order.append(a[0])
if a and a[0] == "set-central-config"
else None,
side_effect=lambda *a, **k: order.append(a[0]) if a and a[0] == "enroll" else None,
),
patch.object(
guest.DeploySiteInputs,
Expand All @@ -668,9 +666,9 @@ def test_site_main_writes_central_config_after_admin_domain(self) -> None:
warm_vm_uuid="",
admin_domain="acme-pilot.blr1.frappe.dev",
central_endpoint="https://central.example",
central_auth_token="tok",
bootstrap_token="tok",
),
),
):
guest.main()
self.assertEqual(order, ["admin-domain", "set-central-config"])
self.assertEqual(order, ["admin-domain", "enroll"])
17 changes: 11 additions & 6 deletions atlas/public/js/setup_wizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,17 @@ function atlas_setup_slides() {
label: __("PowerDNS API URL"),
fieldtype: "Data",
depends_on: "eval:doc.setup_tls && doc.dns_provider_type=='PowerDNS'",
mandatory_depends_on: "eval:doc.setup_tls && doc.dns_provider_type=='PowerDNS'",
mandatory_depends_on:
"eval:doc.setup_tls && doc.dns_provider_type=='PowerDNS'",
description: __("Base URL without /api/v1."),
},
{
fieldname: "powerdns_api_key",
label: __("PowerDNS API Key"),
fieldtype: "Password",
depends_on: "eval:doc.setup_tls && doc.dns_provider_type=='PowerDNS'",
mandatory_depends_on: "eval:doc.setup_tls && doc.dns_provider_type=='PowerDNS'",
mandatory_depends_on:
"eval:doc.setup_tls && doc.dns_provider_type=='PowerDNS'",
},
{
fieldname: "powerdns_server_id",
Expand Down Expand Up @@ -365,10 +367,13 @@ function atlas_provider_slide_onload(slide) {
// the first/declared option visually but leaves the doc value empty, so a field a
// user never touches would post blank and fail its `mandatory_depends_on` — make
// "what you see selected" == "what gets posted".
["provider_type", "do_region", "dns_provider_type", "powerdns_server_id"].forEach((fieldname) => {
const field = slide.get_field(fieldname);
if (field?.df.default && !slide.get_value(fieldname)) field.set_input(field.df.default);
});
["provider_type", "do_region", "dns_provider_type", "powerdns_server_id"].forEach(
(fieldname) => {
const field = slide.get_field(fieldname);
if (field?.df.default && !slide.get_value(fieldname))
field.set_input(field.df.default);
}
);
slide.form.refresh();

slide
Expand Down
4 changes: 3 additions & 1 deletion atlas/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ def setup_tls_layer(tls: dict) -> None:
acme_directory_url=tls.get("acme_directory_url")
or "https://acme-staging-v02.api.letsencrypt.org/directory",
)
frappe.db.set_single_value("Atlas Settings", "dns_provider_type", dns_provider_type, update_modified=False)
frappe.db.set_single_value(
"Atlas Settings", "dns_provider_type", dns_provider_type, update_modified=False
)
frappe.db.set_single_value("Atlas Settings", "tls_provider_type", "Let's Encrypt", update_modified=False)
if not frappe.db.exists("Root Domain", tls["domain"]):
frappe.get_doc(
Expand Down
4 changes: 3 additions & 1 deletion atlas/tests/e2e/use_cases/tls_issuance.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ def _seed_dns_provider_settings(config: dict) -> None:
if config["dns_provider_type"] == "PowerDNS":
powerdns = config["powerdns"]
frappe.db.set_single_value("PowerDNS Settings", "api_url", powerdns["api_url"], update_modified=False)
frappe.db.set_single_value("PowerDNS Settings", "server_id", powerdns["server_id"], update_modified=False)
frappe.db.set_single_value(
"PowerDNS Settings", "server_id", powerdns["server_id"], update_modified=False
)
frappe.utils.password.set_encrypted_password(
"PowerDNS Settings", "PowerDNS Settings", powerdns["api_key"], "api_key"
)
Expand Down
12 changes: 6 additions & 6 deletions atlas/tests/test_api_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,25 +96,25 @@ def test_duplicate_subdomain_throws_clean_taken(self) -> None:
site_api.create_site(team=TEAM, subdomain="acme")
self.assertIn("already taken", str(raised.exception))

def test_bench_credential_rides_the_provision_job_not_the_site(self) -> None:
"""The pilot credential is bench-level: create_site threads it into the provision
def test_bench_seed_rides_the_provision_job_not_the_site(self) -> None:
"""The pilot enrollment seed is bench-level: create_site threads it into the provision
job (→ VM + bench.toml), never onto the Site row, and never into _mirror."""
with patch("frappe.enqueue") as enqueue:
result = site_api.create_site(
team=TEAM,
subdomain="acme",
pilot_credential_id="pcred-abc",
central_endpoint="https://central.test",
central_auth_token="s3cret-token",
bootstrap_token="boot-token",
)
job = next(c.kwargs for c in enqueue.call_args_list if c.args and "auto_provision" in c.args[0])
self.assertEqual(job["pilot_credential_id"], "pcred-abc")
self.assertEqual(job["central_endpoint"], "https://central.test")
self.assertEqual(job["central_auth_token"], "s3cret-token")
self.assertEqual(job["bootstrap_token"], "boot-token")
# Nothing central-flavoured is persisted on the Site, and the token never leaks
# into the mirror Central reflects.
self.assertNotIn("central_auth_token", result)
self.assertFalse(frappe.get_meta("Site").get_field("central_auth_token"))
self.assertNotIn("bootstrap_token", result)
self.assertFalse(frappe.get_meta("Site").get_field("bootstrap_token"))


class TestGetSite(IntegrationTestCase):
Expand Down
6 changes: 3 additions & 3 deletions atlas/tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,11 @@ def test_tls_block_seeds_route53_le_and_root_domain(self) -> None:

def test_tls_block_seeds_powerdns_le_and_root_domain(self) -> None:
setup.run(_do_config(tls=_POWERDNS_TLS_BLOCK))
self.assertEqual(frappe.db.get_single_value("PowerDNS Settings", "api_url"), "https://pdns.example.test")
self.assertEqual(frappe.db.get_single_value("PowerDNS Settings", "server_id"), "primary")
self.assertEqual(
get_secret("PowerDNS Settings", "PowerDNS Settings", "api_key"), "powerdns-secret"
frappe.db.get_single_value("PowerDNS Settings", "api_url"), "https://pdns.example.test"
)
self.assertEqual(frappe.db.get_single_value("PowerDNS Settings", "server_id"), "primary")
self.assertEqual(get_secret("PowerDNS Settings", "PowerDNS Settings", "api_key"), "powerdns-secret")
self.assertEqual(frappe.db.get_single_value("Atlas Settings", "dns_provider_type"), "PowerDNS")
self.assertEqual(frappe.db.get_single_value("Atlas Settings", "tls_provider_type"), "Let's Encrypt")
self.assertTrue(frappe.db.exists("Root Domain", "blr1.frappe.dev"))
Expand Down
Loading
Loading