Skip to content
Closed
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
10 changes: 9 additions & 1 deletion dag/gunbc/auth/approval_decision_store.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive trust from the broker's actual bind

When the role is moved to the newly tested srv2_deploy_deployment, this selects its dashboard bind of 0.0.0.0 (roadmap_dashboard_instance.dag:1210), so approval_identity_trust_for returns IdentityHeaderTrustNotEstablished and every redemption is rejected at approval_decision_store.dag:593-596. The broker itself still binds approval_broker_listen_host, independently of the dashboard instance (live_deploy/spec.dag:547-560), so tying trust to the holder's dashboard bind makes the moved broker unable to approve or deny anything even when its actual loopback/front-door topology is safe.

Useful? React with 👍 / 👎.

Absent => srv1_dashboard_listen_host
}
}

data approval_identity_trust: IdentityTrustStanding = approval_identity_trust_for(listen_host: approval_identity_trust_listen_host())

// ── the store ──────────────────────────────────────────────────────────────────────────────────

Expand Down
7 changes: 5 additions & 2 deletions dag/gunbc/auth/approval_request_client.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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)),
Expand Down
6 changes: 5 additions & 1 deletion dag/gunbc/dispatch_preflight.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion dag/gunbc/dispatch_selection.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
52 changes: 44 additions & 8 deletions dag/gunbc/fabric/fabric_storage_placement.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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? {
Expand All @@ -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,
Comment on lines +123 to +124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Provision fabric directories on the selected host

With prod_role_selection moved to srv2, this placement makes the srv2 spec emit the fabric service and route, but deployment_srv2_target still has ensured_directories: [] (live_deploy/desired.dag:101-112); the socket and store directories are only added by deployment_ensured_srv1_host_directories (live_deploy/spec.dag:1189-1200). On a clean srv2 deployment, the selected store's fabric-storage-door parent and store areas therefore do not exist, so the service cannot bind its Unix socket or open its object/head roots. The fabric directory dependencies need to follow the selected placement rather than remaining srv1-only.

Useful? React with 👍 / 👎.

endpoint: e,
Comment on lines +122 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve fabric state when moving the placement

When prod_role_selection moves from srv1 to srv2, this changes the durable store root from srv1's instance-root child to srv2's distinct instance-root child. Even after the srv2 directory-provisioning issue is fixed, the existing objects and heads remain in the ensured, non-retracted directory on srv1, and a repository-wide search finds no transfer or attachment step before the endpoint flips. The new service therefore exposes an empty fabric history, making existing heads unavailable; moving the role must migrate or reattach the durable store before selecting the new root.

Useful? React with 👍 / 👎.

}
}
}
}

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
Expand Down
2 changes: 1 addition & 1 deletion dag/gunbc/live_deploy/emit.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
40 changes: 25 additions & 15 deletions dag/gunbc/live_deploy/spec.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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 }
Comment on lines +425 to +427

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile the previous broker owner

When prod_role_selection moves away from srv1 or becomes NoProdRole, this only makes the old spec a non-owner; both non-owner branches in live_deploy/emit.dag:1771-1792 return an empty step list, and the host-singleton broker unit is deliberately excluded from owned-artifact reconciliation. Consequently, the previously enabled srv1 broker keeps running from its immutable old release while a moved role can start another broker, leaving two processes with separate local approval stores (or leaving a broker running when production was turned off). The ownership transition needs an explicit stop/disable/removal action for the former host.

Useful? React with 👍 / 👎.

Comment on lines +424 to +427

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Provision approval keys on the selected broker host

When the role is moved to srv2, this makes the srv2 instance own and start the broker, but auth/approval_keyring_converge.dag:92-100 still explicitly refuses every host except srv1 and its fleet endpoint is also fixed to srv1. Independently of cleaning up the old owner, a clean srv2 therefore lacks /etc/gunbc-roadmap/approval-mac-key and the submission/receipt keys, so the newly selected broker refuses filing and redemption. Key and store provisioning must follow the same selected host before ownership moves.

Useful? React with 👍 / 👎.

}
}

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"

Expand Down Expand Up @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions dag/gunbc/roadmap/roadmap_dashboard_instance.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 23 additions & 2 deletions dag/gunbc/roadmap/roadmap_dashboard_instance_apply.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }
Comment on lines +800 to +802

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude the target instance from production peer selection

If prod_role_selection is moved to srv1_lab_codex_feedback_deployment, the srv1 lab apply passes its own host here and receives itself as ProductionPeer. dashboard_instance_apply_preflight then checks dashboard_instances_have_disjoint_owned_paths(instance, production), which is false for the same instance, so every preflight and apply refuses at isolation-preflight. Since the role type permits this built-in deployment, peer selection needs the target instance identity and must not classify the target itself as a peer.

Useful? React with 👍 / 👎.

} 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 }
Expand Down Expand Up @@ -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),
Comment on lines 1989 to +1992

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply role-based peer selection to the srv2 lab

This update wires the new role-derived peer lookup only into the srv1 lab entries. With the newly tested role assignment to srv2_deploy_deployment, srv2_lab_dashboard_apply and its preflight at lines 2065-2075 still pass a hard-coded NoProductionPeer, so they skip the production liveness readback even though the selected production dashboard is now on the same host. Those srv2 entry points must use the same lookup or an apply can report success without detecting that it disrupted the srv2 production process.

Useful? React with 👍 / 👎.

)
}

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),
)
}

Expand Down
Loading
Loading