From 76d7fb54fb4e24d02ef6704f8e8cf17c87031efd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 9 Oct 2026 17:35:22 +0000 Subject: [PATCH] WIP: Deployment risk conformance: one environment model for the repo --- dag/gunbc/auth/approval_decision_store.dag | 10 ++- dag/gunbc/auth/approval_request_client.dag | 7 +- dag/gunbc/dispatch_preflight.dag | 6 +- dag/gunbc/dispatch_selection.dag | 6 +- dag/gunbc/fabric/fabric_storage_placement.dag | 52 +++++++++++-- dag/gunbc/live_deploy/emit.dag | 2 +- dag/gunbc/live_deploy/spec.dag | 40 ++++++---- .../roadmap/roadmap_dashboard_instance.dag | 27 +++++++ .../roadmap_dashboard_instance_apply.dag | 25 ++++++- .../claim/deployment_risk_witness_test.dag | 75 +++++++++++++++++++ 10 files changed, 219 insertions(+), 31 deletions(-) diff --git a/dag/gunbc/auth/approval_decision_store.dag b/dag/gunbc/auth/approval_decision_store.dag index 8038f37b3ae..3fb894a5540 100644 --- a/dag/gunbc/auth/approval_decision_store.dag +++ b/dag/gunbc/auth/approval_decision_store.dag @@ -21,6 +21,7 @@ import extdeps.tailscale.identity { TailnetIdentity } import extdeps.network.address { IpV4, ip_address_is_loopback } import extdeps.network.ipv4 { parse_ipv4_address, Ipv4Parsed, Ipv4ParseFailed } import gunbc.srv1_dashboard_bind { srv1_dashboard_listen_host } +import gunbc.roadmap_dashboard_instance { prod_role_holder_dashboard_instance_from_row } import extdeps.crypto.mac { MacKey, MacKeyId, MacSuite, HmacSha256, MacVerification, mac_verify, mac_key_material_hex_length } import extdeps.languages.json.emit { JsonValue, JsonString, JsonBool, serialize_json, json_object, json_kv, json_string, json_bool } import extdeps.languages.json.parse { JsonDocumentParsed, JsonDocumentUnreadable, JsonMemberFound, JsonMemberAbsent, JsonMemberDuplicated, JsonMemberNotAnObject, parse_json_document, json_object_unique_member } @@ -191,7 +192,14 @@ fn approval_identity_trust_for(listen_host: NonEmptyStr) -> IdentityTrustStandin } } -data approval_identity_trust: IdentityTrustStanding = approval_identity_trust_for(listen_host: srv1_dashboard_listen_host) +fn approval_identity_trust_listen_host() -> NonEmptyStr { + match prod_role_holder_dashboard_instance_from_row() { + Present { value: holder } => holder.listen_host + Absent => srv1_dashboard_listen_host + } +} + +data approval_identity_trust: IdentityTrustStanding = approval_identity_trust_for(listen_host: approval_identity_trust_listen_host()) // ── the store ────────────────────────────────────────────────────────────────────────────────── diff --git a/dag/gunbc/auth/approval_request_client.dag b/dag/gunbc/auth/approval_request_client.dag index f395f4b43fd..9dd3be1e01e 100644 --- a/dag/gunbc/auth/approval_request_client.dag +++ b/dag/gunbc/auth/approval_request_client.dag @@ -16,7 +16,7 @@ import gunbc.auth.approval_request_submission { approval_submission_mac_key_id, sign_submission, } import gunbc.fleet_posix_accounts { fleet_operator_email } -import gunbc.roadmap_dashboard_instance { srv1_live_dashboard_instance } +import gunbc.roadmap_dashboard_instance { prod_role_holder_dashboard_instance_from_row, srv1_live_dashboard_instance } import gunbc.auth.approval_writer_authority { approval_writer_authority, approval_writer_process, ApprovalServingProcess, RoadmapProcess, BrokerProcess, } @@ -36,7 +36,10 @@ data approval_request_submit_path: NonEmptyStr = approval_broker_submit_path fn approval_loopback_origin_for(writer: ApprovalServingProcess) -> NonEmptyStr { match writer { RoadmapProcess => { - let instance = srv1_live_dashboard_instance() + let instance = match prod_role_holder_dashboard_instance_from_row() { + Present { value: holder } => holder + Absent => srv1_live_dashboard_instance() + } concat( concat("http://", instance.listen_host as String), concat(":", to_string(instance.listen_port)), diff --git a/dag/gunbc/dispatch_preflight.dag b/dag/gunbc/dispatch_preflight.dag index 5e573b7cb81..389c86b625a 100644 --- a/dag/gunbc/dispatch_preflight.dag +++ b/dag/gunbc/dispatch_preflight.dag @@ -6,6 +6,7 @@ import gunbc.host_layout { belt_spawn_workdir_env_var } import gunbc.roadmap_dashboard_instance { HostDashboardInstance, srv1_live_dashboard_instance, + prod_role_holder_dashboard_instance_from_row, DashboardInstanceLookup, DashboardInstanceResolved, DashboardInstanceRootUnknown, @@ -320,7 +321,10 @@ fn dispatch_preflight_json_for_instance(instance: HostDashboardInstance) -> Stri } fn dispatch_preflight_json() -> String { - dispatch_preflight_json_for_instance(instance: srv1_live_dashboard_instance()) + match prod_role_holder_dashboard_instance_from_row() { + Present { value: holder } => dispatch_preflight_json_for_instance(instance: holder) + Absent => dispatch_preflight_json_for_instance(instance: srv1_live_dashboard_instance()) + } } fn dispatch_preflight_cli_for_instance(instance: HostDashboardInstance) -> ProcessExit { diff --git a/dag/gunbc/dispatch_selection.dag b/dag/gunbc/dispatch_selection.dag index 42dceb66b2d..4ceb0f0cd91 100644 --- a/dag/gunbc/dispatch_selection.dag +++ b/dag/gunbc/dispatch_selection.dag @@ -66,6 +66,7 @@ import gunbc.roadmap_dashboard_instance { dashboard_instance_provider_state_root, dashboard_instance_provider_executable, srv1_live_dashboard_instance, + prod_role_holder_dashboard_instance_from_row, srv2_lab_dashboard_instance, srv2_dashboard_lab_id, srv2_dashboard_preview_id, @@ -955,7 +956,10 @@ fn declared_provider_inventory_for_instance(instance: HostDashboardInstance) -> ), } } else { - if instance.instance_id == srv1_live_dashboard_instance().instance_id { + if match prod_role_holder_dashboard_instance_from_row() { + Present { value: holder } => instance.instance_id == holder.instance_id + Absent => false + } { ProviderInventory { offers: concat( codex_inventory_offers_for_instance(instance: instance), diff --git a/dag/gunbc/fabric/fabric_storage_placement.dag b/dag/gunbc/fabric/fabric_storage_placement.dag index bebf0d3a505..8ed21b419b7 100644 --- a/dag/gunbc/fabric/fabric_storage_placement.dag +++ b/dag/gunbc/fabric/fabric_storage_placement.dag @@ -5,8 +5,14 @@ import gunbc.fabric_storage_address { fabric_storage_route_prefix, fabric_storag import std.types { String, NonEmptyStr, FilePath, Int, Bool, Port, List } import product.host_identity { HostIdentity } import gunbc.fleet_intent_network { fleet_intent_network } -import gunbc.fleet_host_identity { operator_host_srv1 } -import gunbc.roadmap_dashboard_instance { HostDashboardInstance, dashboard_instance_fabric_storage_root, srv1_live_dashboard_instance, dashboard_instance_child } +import gunbc.roadmap_dashboard_instance { + HostDashboardInstance, + dashboard_instance_fabric_storage_root, + dashboard_instance_child, + prod_role_holder_dashboard_instance, + srv1_live_dashboard_instance, +} +import gunbc.deployment_risk { ProdRoleSelection, prod_role_selection } import std.effect_grant { Read, Write, Execute } import extdeps.tailscale.serve { tailscale_serve_unix_proxy_peer } import gunbc.ownership { Ensured } @@ -67,19 +73,37 @@ data fabric_storage_door_proxy_peer: NonEmptyStr = tailscale_serve_unix_proxy_pe // The store root is the placed instance's fabric DB root -- one authority with the deployment that -// creates it (gunbc.roadmap_dashboard_instance dashboard_instance_fabric_storage_root). +// creates it (gunbc.roadmap_dashboard_instance dashboard_instance_fabric_storage_root). Placement +// follows the prod-role holder. Door and store-root PATH helpers still need a HostDashboardInstance +// when the live row is NoProdRole; that arm names srv1_live only as a path constructor residual +// (DESIGN 3b stated divergence). The placement decision itself is Unplaced there and never treats +// that residual as ownership. +fn fabric_storage_placed_instance_under(selection: ProdRoleSelection) -> HostDashboardInstance? { + prod_role_holder_dashboard_instance(selection: selection) +} + fn fabric_storage_placed_instance() -> HostDashboardInstance { - srv1_live_dashboard_instance() + match fabric_storage_placed_instance_under(selection: prod_role_selection) { + Present { value: i } => i + Absent => srv1_live_dashboard_instance() + } } fn fabric_storage_store_root() -> NonEmptyStr { dashboard_instance_fabric_storage_root(instance: fabric_storage_placed_instance()) as String as NonEmptyStr } +fn fabric_storage_placed_on_under(selection: ProdRoleSelection, instance: HostDashboardInstance) -> Bool { + match fabric_storage_placed_instance_under(selection: selection) { + Absent => false + Present { value: holder } => (instance.instance_id as String) == (holder.instance_id as String) + } +} + // WHETHER AN INSTANCE HOLDS THE PLACEMENT: the deployment owns the store root and the endpoint only // there. Identity is the instance's own id, not a path comparison. fn fabric_storage_placed_on(instance: HostDashboardInstance) -> Bool { - (instance.instance_id as String) == (fabric_storage_placed_instance().instance_id as String) + fabric_storage_placed_on_under(selection: prod_role_selection, instance: instance) } fn fabric_storage_endpoint_for(host: HostIdentity) -> NonEmptyStr? { @@ -89,13 +113,25 @@ fn fabric_storage_endpoint_for(host: HostIdentity) -> NonEmptyStr? { } } -fn fabric_storage_placement() -> FabricStoragePlacement { - match fabric_storage_endpoint_for(host: operator_host_srv1) { +fn fabric_storage_placement_under(selection: ProdRoleSelection) -> FabricStoragePlacement { + match fabric_storage_placed_instance_under(selection: selection) { Absent => FabricStorageUnplaced - Present { value: e } => FabricStoragePlaced { host: operator_host_srv1, store_root: fabric_storage_store_root(), endpoint: e } + Present { value: instance } => + match fabric_storage_endpoint_for(host: instance.host_identity as HostIdentity) { + Absent => FabricStorageUnplaced + Present { value: e } => FabricStoragePlaced { + host: instance.host_identity as HostIdentity, + store_root: dashboard_instance_fabric_storage_root(instance: instance) as String as NonEmptyStr, + endpoint: e, + } + } } } +fn fabric_storage_placement() -> FabricStoragePlacement { + fabric_storage_placement_under(selection: prod_role_selection) +} + // ── WHO WRITES THE STORE, AND SO WHAT ITS DIRECTORIES ARE ──────────────────────────────────────── // // ONE PRINCIPAL WRITES THIS STORE'S FILES: the served endpoint (gunbc.live_deploy.emit diff --git a/dag/gunbc/live_deploy/emit.dag b/dag/gunbc/live_deploy/emit.dag index ddf5413f7a0..23e75ddc0d4 100644 --- a/dag/gunbc/live_deploy/emit.dag +++ b/dag/gunbc/live_deploy/emit.dag @@ -3338,7 +3338,7 @@ fn approval_broker_dark_install_intent( // -- a stage that reports success having installed nothing, which is the absorbing fallback DESIGN // section 5 forbids at exactly the seam where the operator is being told a stage completed. The // marker is not valid shell, so the stage fails and names the owner. -data approval_broker_dark_install_not_owner_poison: String = "__GUNBC_DEPLOY_REFUSED__ approval-broker DARK INSTALL was reached with a deployment that does not own this host's broker unit. gunbc-approval-broker.service is a HOST singleton owned by gunbc.live_deploy.spec gunbc_approval_broker_owning_instance; a twin apply installing it would point the host's broker at the twin's tree and restart it over production's process (review 68094). This marker is not valid deploy shell, so the stage fails loudly rather than emitting a script that installs nothing and reports success. instance=" +data approval_broker_dark_install_not_owner_poison: String = "__GUNBC_DEPLOY_REFUSED__ approval-broker DARK INSTALL was reached with a deployment that does not own this host's broker unit. gunbc-approval-broker.service is a HOST singleton owned by the prod-role holder (gunbc.deployment_risk prod_role_selection, via gunbc.live_deploy.spec instance_owns_the_host_approval_broker); a twin apply installing it would point the host's broker at the twin's tree and restart it over production's process (review 68094). This marker is not valid deploy shell, so the stage fails loudly rather than emitting a script that installs nothing and reports success. instance=" data approval_broker_dark_install_emit_refused_poison: String = "__GUNBC_ORCH_EMIT_REFUSED__ approval-broker DARK INSTALL intent emission was rejected by v2.compiler.emit_orchestration; this marker is not valid deploy shell, so stage 1 of the cutover fails loudly rather than a hand-spelled fallback masking the refusal (DESIGN section 5: refuse, never widen)\n" diff --git a/dag/gunbc/live_deploy/spec.dag b/dag/gunbc/live_deploy/spec.dag index 1bc03c1b494..95a579f13be 100644 --- a/dag/gunbc/live_deploy/spec.dag +++ b/dag/gunbc/live_deploy/spec.dag @@ -51,7 +51,9 @@ import gunbc.roadmap_dashboard_instance { dashboard_instance_compute_root, dashboard_instance_provider_state_root, dashboard_instance_tailnet_door_socket, + prod_role_holder_dashboard_instance, } +import gunbc.deployment_risk { ProdRoleSelection, prod_role_selection } import extdeps.http.server { HttpServerListenConfig } import gunbc.auth.approval_broker_endpoint { approval_broker_listen_port, approval_broker_listen_host, approval_broker_confirm_front_door } import gunbc.auth.approval_broker_cutover { approval_front_door_mounts_installed, approval_broker_front_door_endpoints } @@ -414,20 +416,21 @@ data approval_broker_serve_entry_file: NonEmptyStr = "dag/gunbc/auth/approval_br // // SO OWNERSHIP IS DECLARED AND THE EMITTER READS IT. One instance per host owns the broker unit; // every other spec emits no broker step at all, writes nothing and restarts nothing. -// THE INSTANCE THAT OWNS THE HOST'S BROKER. It is srv1-live because that is where the approval -// store is: gunbc.auth.approval_decision_store approval_decision_store_root is one path on one -// host, and the whole point of a single-writer store is that exactly one process opens it. -// -// IT IS A DECLARED OWNER RATHER THAN A DERIVATION FROM THE INSTANCE because "which deployment runs -// the approval loop" is not recoverable from any field an instance carries -- srv1-live and -// srv1-lab are the same shape on the same host, and the only thing that distinguishes them for this -// purpose is that one of them was chosen. Deriving it would mean inventing a rule that happens to -// select production today. -// -// A SECOND HOST SERVING APPROVALS IS WHAT RETIRES THIS ROW, at which point the owner becomes a -// per-host lookup and the store root becomes host-derived with it. Until then a single row is the -// honest shape: there is one approval loop, and it has one home. -data gunbc_approval_broker_owning_instance: NonEmptyStr = "srv1-live" +// THE INSTANCE THAT OWNS THE HOST'S BROKER follows the prod role. The store is still one path on +// one host (gunbc.auth.approval_decision_store approval_decision_store_root); which instance may +// install or restart the unit is the deployment that holds ProdRole, not a constructor name. +// Moving prod_role_selection moves the owner. NoProdRole means nobody owns the unit -- a twin +// must not install it there either. +fn gunbc_approval_broker_owning_instance_under(selection: ProdRoleSelection) -> NonEmptyStr? { + match prod_role_holder_dashboard_instance(selection: selection) { + Absent => none + Present { value: holder } => Present { value: holder.instance_id } + } +} + +fn gunbc_approval_broker_owning_instance() -> NonEmptyStr? { + gunbc_approval_broker_owning_instance_under(selection: prod_role_selection) +} data gunbc_approval_broker_unit_name: NonEmptyStr = "gunbc-approval-broker.service" @@ -456,8 +459,15 @@ fn deployment_owns_the_host_approval_broker(spec: DeploymentSpec) -> Bool { // THE ONE OWNERSHIP COMPARISON, read by the spec-level predicate above and by the front-door member, // which is built before a spec exists and so holds only the instance. +fn instance_owns_the_host_approval_broker_under(selection: ProdRoleSelection, instance_id: NonEmptyStr) -> Bool { + match gunbc_approval_broker_owning_instance_under(selection: selection) { + Absent => false + Present { value: owner } => (instance_id as String) == (owner as String) + } +} + fn instance_owns_the_host_approval_broker(instance_id: NonEmptyStr) -> Bool { - (instance_id as String) == (gunbc_approval_broker_owning_instance as String) + instance_owns_the_host_approval_broker_under(selection: prod_role_selection, instance_id: instance_id) } fn gunbc_approval_broker_unit_path() -> NonEmptyStr { diff --git a/dag/gunbc/roadmap/roadmap_dashboard_instance.dag b/dag/gunbc/roadmap/roadmap_dashboard_instance.dag index 7e72e070ab6..b8bf575ddbc 100644 --- a/dag/gunbc/roadmap/roadmap_dashboard_instance.dag +++ b/dag/gunbc/roadmap/roadmap_dashboard_instance.dag @@ -10,6 +10,9 @@ import gunbc.ensure { EnsureObserveOnly } import gunbc.deployment_risk { DeploymentId, DeploymentRiskClass, + ProdRoleSelection, + NoProdRole, + ProdRoleHeldBy, deployment_risk_class, prod_role_selection, srv1_daily_workspace_deployment, @@ -275,6 +278,30 @@ fn dashboard_instance_risk_class(instance: HostDashboardInstance) -> DeploymentR deployment_risk_class(selection: prod_role_selection, deployment: instance.deployment) } +// THE INSTANCE THAT REALIZES A DeploymentId IN THIS REPOSITORY'S PUBLIC BUILT-IN SET. Role-following +// singletons (broker owner, fabric placement) resolve the prod holder through this fold and the +// selection, never by spelling srv1_live. An id this roster does not carry is Absent -- it is not +// guessed from a constructor name. +fn dashboard_instance_for_deployment(deployment: DeploymentId) -> HostDashboardInstance? { + public_builtin_dashboard_instances() + |> filter(i => i.deployment == deployment) + |> first +} + +// THE DASHBOARD INSTANCE THAT HOLDS THE PROD ROLE UNDER A SELECTION, or Absent when the role is off +// or names a deployment this roster does not realize. Parameterized so a fixture can move the role +// without editing the live row (deployment-risk D2 RED). +fn prod_role_holder_dashboard_instance(selection: ProdRoleSelection) -> HostDashboardInstance? { + match selection { + NoProdRole => none + ProdRoleHeldBy { deployment: d } => dashboard_instance_for_deployment(deployment: d) + } +} + +fn prod_role_holder_dashboard_instance_from_row() -> HostDashboardInstance? { + prod_role_holder_dashboard_instance(selection: prod_role_selection) +} + // The fleet's authority, unchanged: origin advertises refs/fleet/desired. Named here rather than // spelled at each instance so that the fleet answer stays one value across every fleet host. data fleet_admitted_revision_source: DashboardAdmittedRevisionSource = DashboardAdmittedRevisionSource { diff --git a/dag/gunbc/roadmap/roadmap_dashboard_instance_apply.dag b/dag/gunbc/roadmap/roadmap_dashboard_instance_apply.dag index b46a61a743a..0a1cbfcd013 100644 --- a/dag/gunbc/roadmap/roadmap_dashboard_instance_apply.dag +++ b/dag/gunbc/roadmap/roadmap_dashboard_instance_apply.dag @@ -3,6 +3,7 @@ module gunbc.roadmap_dashboard_instance_apply import std.algebra { trim } import std.types { String, NonEmptyStr, Bool, List, FilePath, GitRef, CommitSha } +import gunbc.deployment_risk { ProdRoleSelection, prod_role_selection } import std.process { ProcessExit, ExitSuccess, ExitFailure, exit_failure } import extdeps.shell import extdeps.filesystem.filesystem_io @@ -93,6 +94,7 @@ import gunbc.roadmap_dashboard_instance { DashboardInstanceRefused, srv1_live_dashboard_instance, srv1_lab_dashboard_instance, + prod_role_holder_dashboard_instance, srv2_lab_dashboard_instance, dashboard_instance_provider_state_root, dashboard_instance_provider_executable, @@ -789,6 +791,25 @@ type DashboardProductionPeer = ProductionPeer { instance: HostDashboardInstance } | NoProductionPeer { host: NonEmptyStr } +// Isolation and liveness compare against the prod-role holder on the SAME HOST, never against a +// constructor named "live". A holder on another host is not a peer of this apply; NoProdRole is +// unpeered. Same-host TestRisk applies still refuse overlapping owned paths with whoever holds prod. +fn dashboard_production_peer_on_host_under(selection: ProdRoleSelection, host: NonEmptyStr) -> DashboardProductionPeer { + match prod_role_holder_dashboard_instance(selection: selection) { + Absent => NoProductionPeer { host: host } + Present { value: holder } => + if (holder.host_identity as String) == (host as String) { + ProductionPeer { instance: holder } + } else { + NoProductionPeer { host: host } + } + } +} + +fn dashboard_production_peer_on_host(host: NonEmptyStr) -> DashboardProductionPeer { + dashboard_production_peer_on_host_under(selection: prod_role_selection, host: host) +} + type DashboardProductionReading = ProductionRead { snapshot: DashboardProductionSnapshot } | ProductionUnpeered { host: NonEmptyStr } @@ -1968,14 +1989,14 @@ fn srv2_deploy_ensure_roots_cli() -> ProcessExit { fn srv1_lab_dashboard_apply() -> DashboardInstanceApplyResult { dashboard_instance_apply( instance: srv1_lab_dashboard_instance(), - production: ProductionPeer { instance: srv1_live_dashboard_instance() }, + production: dashboard_production_peer_on_host(host: srv1_lab_dashboard_instance().host_identity), ) } fn srv1_lab_dashboard_preflight() -> DashboardApplyPreflightResult { dashboard_instance_apply_preflight( instance: srv1_lab_dashboard_instance(), - production: ProductionPeer { instance: srv1_live_dashboard_instance() }, + production: dashboard_production_peer_on_host(host: srv1_lab_dashboard_instance().host_identity), ) } diff --git a/dag/test/claim/deployment_risk_witness_test.dag b/dag/test/claim/deployment_risk_witness_test.dag index 1cd9f422a0d..9289f3fdfa3 100644 --- a/dag/test/claim/deployment_risk_witness_test.dag +++ b/dag/test/claim/deployment_risk_witness_test.dag @@ -31,6 +31,8 @@ import gunbc.recurring_failure_mode.a_test_deployment_acts_on_the_prod_deploymen import gunbc.roadmap_dashboard_instance { HostDashboardInstance, dashboard_instance_risk_class, + dashboard_instance_for_deployment, + prod_role_holder_dashboard_instance, macbook_local_dashboard_instance, srv1_live_dashboard_instance, srv1_lab_dashboard_instance, @@ -38,6 +40,19 @@ import gunbc.roadmap_dashboard_instance { srv2_deploy_dashboard_instance, srv2_lab_dashboard_instance, } +import gunbc.live_deploy.spec { instance_owns_the_host_approval_broker_under, instance_owns_the_host_approval_broker } +import gunbc.fabric_storage_placement { + fabric_storage_placed_on_under, + fabric_storage_placed_on, + fabric_storage_placement_under, + FabricStoragePlaced, + FabricStorageUnplaced, +} +import gunbc.roadmap_dashboard_instance_apply { + dashboard_production_peer_on_host_under, + ProductionPeer, + NoProductionPeer, +} data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -131,3 +146,63 @@ test fn exactly_one_dashboard_instance_derives_prod_risk_and_it_is_the_daily_wor test fn the_wrong_instance_failure_mode_row_cites_this_red() -> Bool { any(a_test_deployment_acts_on_the_prod_deployments_state.evidence, r => r.decl_name == "a_test_deployment_refuses_rather_than_resolving_the_prod_bindings") } + +// ── D2: a fixture assignment moves the role-following singletons ──────────────────────────────── +// THE BRIEF'S RED. Changing the selection, and nothing else, moves broker ownership and fabric +// placement. A mutant that still compared instance_id to "srv1-live" or called +// srv1_live_dashboard_instance() would fail the srv2 conjuncts. +test fn a_fixture_prod_role_move_moves_the_broker_owner_and_fabric_placement() -> Bool { + let moved = ProdRoleHeldBy { deployment: srv2_deploy_deployment } + let srv2 = srv2_deploy_dashboard_instance() + let srv1 = srv1_live_dashboard_instance() + instance_owns_the_host_approval_broker_under(selection: moved, instance_id: srv2.instance_id) + && !instance_owns_the_host_approval_broker_under(selection: moved, instance_id: srv1.instance_id) + && fabric_storage_placed_on_under(selection: moved, instance: srv2) + && !fabric_storage_placed_on_under(selection: moved, instance: srv1) + && match fabric_storage_placement_under(selection: moved) { + FabricStoragePlaced { host: h, store_root: _, endpoint: _ } => (h as String) == (srv2.host_identity as String) + FabricStorageUnplaced => false + } +} + +test fn the_live_row_still_pins_broker_and_fabric_on_the_daily_workspace() -> Bool { + let live = srv1_live_dashboard_instance() + instance_owns_the_host_approval_broker(instance_id: live.instance_id) + && !instance_owns_the_host_approval_broker(instance_id: srv1_lab_dashboard_instance().instance_id) + && fabric_storage_placed_on(instance: live) + && !fabric_storage_placed_on(instance: srv2_deploy_dashboard_instance()) +} + +test fn no_prod_role_owns_no_broker_and_places_no_fabric_store() -> Bool { + !instance_owns_the_host_approval_broker_under( + selection: NoProdRole, + instance_id: srv1_live_dashboard_instance().instance_id, + ) + && !fabric_storage_placed_on_under(selection: NoProdRole, instance: srv1_live_dashboard_instance()) + && match fabric_storage_placement_under(selection: NoProdRole) { + FabricStorageUnplaced => true + FabricStoragePlaced { host: _, store_root: _, endpoint: _ } => false + } + && match prod_role_holder_dashboard_instance(selection: NoProdRole) { + Absent => true + Present { value: _ } => false + } +} + +test fn a_fixture_role_move_makes_srv1_lab_unpeered_from_the_new_prod_host() -> Bool { + let moved = ProdRoleHeldBy { deployment: srv2_deploy_deployment } + match dashboard_production_peer_on_host_under( + selection: moved, + host: srv1_lab_dashboard_instance().host_identity, + ) { + NoProductionPeer { host: h } => (h as String) == (srv1_lab_dashboard_instance().host_identity as String) + ProductionPeer { instance: _ } => false + } +} + +test fn dashboard_instance_for_deployment_answers_all_six_constructors() -> Bool { + all(dashboard_instances(), i => match dashboard_instance_for_deployment(deployment: i.deployment) { + Present { value: found } => found.instance_id == i.instance_id + Absent => false + }) +}