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
12 changes: 9 additions & 3 deletions dag/extdeps/systemd/systemctl.dag
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,14 @@ fn systemctl_set_property_argv(unit: NonEmptyStr, property: NonEmptyStr, value:
]
}

// `systemctl revert UNIT...` takes UNIT NAMES ONLY and removes every drop-in of each named unit
// (systemctl(1), systemd 255). Property names are not arguments: an earlier argv appended
// "MemoryMax" "MemoryHigh", which systemctl read as two more units (MemoryMax.service,
// MemoryHigh.service) and reverted silently with exit 0 -- so the argv claimed a property-scoped
// revert it never performed. What this reverts is the unit's whole drop-in set, including the
// control shadows set-property wrote, and that is what the caller is relying on.
fn systemctl_revert_memory_caps_argv(unit: NonEmptyStr) -> List<String> {
["systemctl", "revert", unit as String, "MemoryMax", "MemoryHigh"]
["systemctl", "revert", unit as String]
}

// THE USER MANAGER IS A DIFFERENT MANAGER, NOT A FLAG ON THE SAME QUERY. `systemctl show` without
Expand Down Expand Up @@ -597,10 +603,10 @@ service systemd.Systemctl {
success: Bool from "exit_success"
stderr: String from "stderr"
}
transport shell { argv: ["systemctl", "revert", "{unit}", "MemoryMax", "MemoryHigh"] }
transport shell { argv: ["systemctl", "revert", "{unit}"] }
exit {
0 => Unit
nonzero => String "systemctl revert MemoryMax MemoryHigh failed"
nonzero => String "systemctl revert failed"
}
mock_response {
0 => { success: true, stderr: "" } "hermetic systemd.Systemctl.RevertMemoryCaps"
Expand Down
18 changes: 18 additions & 0 deletions dag/gunbc/ci/ci_deploy_access.dag
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,21 @@ fn srv1_live_deploy_privileged_commands() -> List<PrivilegedCommand> {
p: fleet_posix_ci_runner_principal()
)
}

// EVERY ACCESS THAT APPLIES A DEPLOYMENT, AS ONE ROSTER. A host-shared resource whose content is a
// function of who deploys to that host -- /etc/sudoers.d/gunbc-deploy -- is derived from this list
// filtered by host, never from the one access that happens to be running, so applying any
// deployment through any access renders the same bytes (gunbc.ci_deploy_sudoers
// deploy_sudoers_dropin_body). A new access is a row here; one left out is a principal whose grants
// the next apply through another access would delete.
fn deploy_access_roster() -> List<DeployAccess> {
[
ci_deploy_srv1_access,
ci_deploy_srv1_operator_access,
ci_deploy_srv1_operator_local_access,
]
}

fn deploy_accesses_on_host(host: ComputeHost) -> List<DeployAccess> {
filter(xs: deploy_access_roster(), predicate: a => (a.host.identity as String) == (host.identity as String))
}
26 changes: 20 additions & 6 deletions dag/gunbc/ci/ci_deploy_sudoers.dag
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module gunbc.ci_deploy_sudoers

import std.types { String, NonEmptyStr, List, Bool }
import v2.std.algebra { fold_list, any }
import v2.std.algebra { fold_list, any, list_flat_map }
import std.list { distinct_by_key }
import extdeps.sudo.elevation { sudo_elevate_argv }
import v2.extdeps.languages.bash_build {
bash_build_command_from_lits,
Expand Down Expand Up @@ -30,6 +31,7 @@ import extdeps.sudo.nopasswd_execute_probe_check_op {
import gunbc.ci_deploy_access {
DeployAccess,
PrivilegedCommand,
deploy_accesses_on_host,
gunbc_deploy_sudoers_dropin,
deploy_access_sudoers_line,
posix_sudo_grant_to_privileged,
Expand Down Expand Up @@ -100,15 +102,27 @@ fn deploy_sudoers_line_for_grant(access: DeployAccess, g: PosixSudoGrant) -> Str
)
}

// One line per grant, header first. The heredoc row (v2.workflow.bash_emit bash_emit_heredoc)
// THE DROP-IN IS HOST-SHARED, SO ITS BODY IS THE UNION OVER EVERY ACCESS THAT DEPLOYS TO THE HOST
// (gunbc.ci_deploy_access deploy_accesses_on_host), not the running access's own grants. Rendering
// from the running access alone made an operator-local apply rewrite the file to the operator's one
// line and delete the CI runner's grants. One line per (principal, grant), first occurrence kept
// in roster order, header first. The heredoc row (v2.workflow.bash_emit bash_emit_heredoc)
// terminates the body itself, so the lines are joined without a trailing newline.
fn deploy_sudoers_dropin_body(access: DeployAccess) -> String {
join(
concat([deploy_sudoers_dropin_header()], map(access.principal.sudo_grants, g => deploy_sudoers_line_for_grant(access: access, g: g))),
"\n"
fn deploy_sudoers_dropin_lines_for(accesses: List<DeployAccess>) -> List<String> {
distinct_by_key(
xs: list_flat_map(xs: accesses, f: a => map(a.principal.sudo_grants, g => deploy_sudoers_line_for_grant(access: a, g: g))),
key: l => l,
)
}

fn deploy_sudoers_dropin_body_for(accesses: List<DeployAccess>) -> String {
join(concat([deploy_sudoers_dropin_header()], deploy_sudoers_dropin_lines_for(accesses: accesses)), "\n")
}

fn deploy_sudoers_dropin_body(access: DeployAccess) -> String {
deploy_sudoers_dropin_body_for(accesses: deploy_accesses_on_host(host: access.host))
}

// THE ELEVATION PREFIX IS READ FROM extdeps.sudo.elevation sudo_elevate_argv, not typed here, so the
// two tokens that cross the privilege boundary have one authority in the repository.
fn deploy_sudoers_elevated(command: List<String>, tail: List<Node>) -> Node {
Expand Down
66 changes: 62 additions & 4 deletions dag/gunbc/live_deploy/apply.dag
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,18 @@ import gunbc.live_deploy.spec {
deployment_plan_host_identity,
deployment_tailnet_door_socket,
}
import gunbc.live_deploy.desired { deployment_spec_srv1 }
import gunbc.live_deploy.desired {
deployment_spec_srv1,
desired_deployment_for_instance_id,
selected_deployment_spec,
srv1_production_desired_deployment,
instance_id_text,
DesiredDeploymentFound,
DesiredDeploymentUnknownInstanceId,
DesiredDeploymentRosterRefused,
DesiredRosterSameHostCollision,
DesiredRosterDuplicateInstanceId,
}
import std.types { String, CommitSha, NonEmptyStr }
import gunbc.live_deploy.candidate {
CandidateRelease,
Expand Down Expand Up @@ -1127,19 +1138,66 @@ fn approval_broker_dark_install_srv1_wet() -> ProcessExit {
)
}

// ONE APPLY ROUTE, SELECTED BY INSTANCE ID. The roster (gunbc.live_deploy.desired
// desired_deployment_roster) is the selection authority; this is the only place an apply entry
// turns an instance id into a DeploymentSpec, and the production entries below are this route
// called with production's id rather than a second fold. A row whose target host is not the host
// the access reaches refuses: an access is a principal on ONE host, and pointing it at another
// row would mutate the wrong machine under a correct-looking spec.
type DesiredApplySelection
= DesiredApplySelected { spec: DeploymentSpec }
| DesiredApplyRefused { reason: String }

fn live_deploy_desired_apply_selection(instance_id: String, access: DeployAccess) -> DesiredApplySelection {
match desired_deployment_for_instance_id(instance_id: instance_id) {
DesiredDeploymentUnknownInstanceId { instance_id: id } =>
DesiredApplyRefused { reason: join(["live_deploy apply: DesiredDeploymentUnknownInstanceId '", id, "' names no row of desired_deployment_roster"], "") }
DesiredDeploymentRosterRefused { refusal } =>
match refusal {
DesiredRosterSameHostCollision { host } =>
DesiredApplyRefused { reason: join(["live_deploy apply: DesiredRosterSameHostCollision on host ", host], "") }
DesiredRosterDuplicateInstanceId { instance_id: dup } =>
DesiredApplyRefused { reason: join(["live_deploy apply: DesiredRosterDuplicateInstanceId ", dup], "") }
}
DesiredDeploymentFound { row } =>
if (row.target.host.identity as String) != (access.host.identity as String) {
DesiredApplyRefused { reason: join(["live_deploy apply: DesiredApplyHostMismatch — row '", instance_id, "' targets ", row.target.host.identity as String, " but the access reaches ", access.host.identity as String], "") }
} else {
DesiredApplySelected { spec: selected_deployment_spec(row: row) }
}
}
}

fn live_deploy_apply_desired_wet(instance_id: String, access: DeployAccess) -> ProcessExit {
match live_deploy_desired_apply_selection(instance_id: instance_id, access: access) {
DesiredApplyRefused { reason } => exit_failure(reason: reason)
DesiredApplySelected { spec } => live_deploy_wet_with_access(spec: spec, access: access)
}
}

// The operator-local apply of any srv1 roster row, e.g.
// `--function live_deploy_apply_desired_operator_local_wet --arg instance_id=srv1-lab-codex-feedback-v0`.
fn live_deploy_apply_desired_operator_local_wet(instance_id: String) -> ProcessExit {
live_deploy_apply_desired_wet(instance_id: instance_id, access: ci_deploy_srv1_operator_local_access)
}

fn srv1_production_instance_id() -> String {
instance_id_text(id: srv1_production_desired_deployment().instance.instance_id)
}

fn live_deploy_apply_srv1_wet() -> ProcessExit {
live_deploy_wet_with_access(spec: deployment_spec_srv1(), access: ci_deploy_srv1_access)
live_deploy_apply_desired_wet(instance_id: srv1_production_instance_id(), access: ci_deploy_srv1_access)
}

fn live_deploy_apply_srv1_operator_wet() -> ProcessExit {
live_deploy_wet_with_access(spec: deployment_spec_srv1(), access: ci_deploy_srv1_operator_access)
live_deploy_apply_desired_wet(instance_id: srv1_production_instance_id(), access: ci_deploy_srv1_operator_access)
}

// The converge verb's deploy mode: the same admitted path the CI entry runs, under the operator
// principal on the host itself. Candidate observation, preflight, mutation and the readiness
// gate are all shared; nothing here is a second deploy.
fn live_deploy_apply_srv1_operator_local_wet() -> ProcessExit {
live_deploy_wet_with_access(spec: deployment_spec_srv1(), access: ci_deploy_srv1_operator_local_access)
live_deploy_apply_desired_operator_local_wet(instance_id: srv1_production_instance_id())
}

fn live_deploy_retract_srv1_wet() -> ProcessExit {
Expand Down
30 changes: 22 additions & 8 deletions dag/gunbc/live_deploy/spec.dag
Original file line number Diff line number Diff line change
Expand Up @@ -1185,22 +1185,36 @@ fn deployment_ensured_srv1_host_directories() -> List<DeploymentDependencyStep>
owner: gunbc_service_user,
},
},
], concat(map(fabric_storage_store_directories(), a => DeploymentDependencyStep {
kind: FabricStorageStoreDirectory,
subject: EnsuredManagedHostDirectory { admission: a },
}), [
], [
DeploymentDependencyStep {
kind: CodexRuntimeReleaseDirectory,
subject: EnsuredHostDirectory {
path: srv1_codex_runtime_release_root as String as NonEmptyStr,
owner: gunbc_service_user,
},
},
]))
])
}

// THE FABRIC STORE IS THE PLACED INSTANCE'S, NOT THE HOST'S (gunbc.fabric_storage_placement
// fabric_storage_placed_instance). While its directories rode on the host target, every deployment
// on srv1 converged them -- a lab twin's apply ran `install -d -m` over production's store. Only the
// placed instance ensures them; any other instance on the host reaches the store through its door
// at most and writes nothing here. Same rule PR #13583 states as
// fabric_storage_directory_demands(instance).
fn fabric_storage_ensured_steps(instance: HostDashboardInstance) -> List<DeploymentDependencyStep> {
if fabric_storage_placed_on(instance: instance) {
map(fabric_storage_store_directories(), a => DeploymentDependencyStep {
kind: FabricStorageStoreDirectory,
subject: EnsuredManagedHostDirectory { admission: a },
})
} else {
[]
}
}

fn deployment_ensured_steps(target: DeploymentHostTarget) -> List<DeploymentDependencyStep> {
concat(deployment_ensured_packages(), target.ensured_directories)
fn deployment_ensured_steps(instance: HostDashboardInstance, target: DeploymentHostTarget) -> List<DeploymentDependencyStep> {
concat(deployment_ensured_packages(), concat(target.ensured_directories, fabric_storage_ensured_steps(instance: instance)))
}

// THE PROVIDER-STATE ROOT IS AN ENSURED MEMBER OF THE INSTANCE, NOT OF THE HOST (CONVERGENCE-ONE
Expand Down Expand Up @@ -1287,7 +1301,7 @@ fn deployment_steps_apply_order(
names: DeploymentNames,
) -> List<DeploymentStep> {
concat(
map(deployment_ensured_steps(target: target), s => Dependency { step: s }),
map(deployment_ensured_steps(instance: instance, target: target), s => Dependency { step: s }),
map(
deployment_owned_steps(instance: instance, names: names),
s => ArtifactStep { step: s },
Expand Down
2 changes: 1 addition & 1 deletion dag/gunbc/roadmap/roadmap_dashboard_instance.dag
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,7 @@ fn srv1_lab_dashboard_instance() -> HostDashboardInstance {
root: srv1_dashboard_lab_root,
serve_function: "roadmap_serve_handle_srv1_lab" as NonEmptyStr,
listen_port: 18081,
source_anchor_ref: "refs/heads/session/codex-provider-feedback-v0" as NonEmptyStr,
source_anchor_ref: "refs/remotes/origin/main" as NonEmptyStr,

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 Fetch main before using its remote-tracking ref

When the controller worktree is non-shallow and its origin/main has not recently been fetched, this points the lab at a stale local remote-tracking ref rather than the current main branch. Preflight captures that stale SHA, and dashboard_hydrate_controller_history explicitly performs no fetch for non-shallow repositories, so the apply bundles the cached commit and can report the lab converged while still serving outdated content. Fetch the modeled remote before resolving/bundling this ref, or otherwise ensure the anchor is refreshed as part of apply.

Useful? React with 👍 / 👎.

actuation: DashboardActuating,
provision_transport: DashboardSshProvision { host: "srv1" as NonEmptyStr },
)
Expand Down
39 changes: 39 additions & 0 deletions dag/test/claim/ci/deploy_sudoers_host_union_witness_test.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module test.claim.ci.deploy_sudoers_host_union_witness_test

import std.types { Bool, String, List }
import gunbc.ci_deploy_access {
DeployAccess,
ci_deploy_srv1_access,
ci_deploy_srv1_operator_access,
ci_deploy_srv1_operator_local_access,
deploy_accesses_on_host,
}
import gunbc.ci_deploy_sudoers { deploy_sudoers_dropin_body, deploy_sudoers_dropin_body_for }

// /etc/sudoers.d/gunbc-deploy IS HOST-SHARED (gunbc.ci_deploy_sudoers deploy_sudoers_dropin_body).
// The drop-in body is a function of the access alone -- no deployment row reaches it -- so the
// lab row and the production row render it identically by construction; what these claims pin is
// that every srv1 access renders the SAME bytes and that those bytes keep the CI runner's grants.
data ghrunner_systemctl_line: String = "ghrunner ALL=(ALL) NOPASSWD: /usr/bin/systemctl"

test fn every_srv1_access_renders_the_same_dropin_and_it_keeps_the_ci_runner_grants() -> Bool {
let local = deploy_sudoers_dropin_body(access: ci_deploy_srv1_operator_local_access)
local == deploy_sudoers_dropin_body(access: ci_deploy_srv1_access)
&& local == deploy_sudoers_dropin_body(access: ci_deploy_srv1_operator_access)
&& string_contains(s: local, pattern: ghrunner_systemctl_line)
&& string_contains(s: local, pattern: "briansrls ALL=(ALL) NOPASSWD: /usr/bin/hostnamectl")
}

fn is_ci_runner_access(a: DeployAccess) -> Bool {
(a.principal.posix.name as String) == (ci_deploy_srv1_access.principal.posix.name as String)
}

// THE DISCRIMINATOR: the same body over the host roster with the CI principal removed loses the
// runner's lines. This is the drop-in the operator-local apply used to install.
test fn removing_the_ci_principal_from_the_union_drops_its_grants() -> Bool {
let without_ci = filter(
xs: deploy_accesses_on_host(host: ci_deploy_srv1_access.host),
predicate: a => !is_ci_runner_access(a: a),
)
!string_contains(s: deploy_sudoers_dropin_body_for(accesses: without_ci), pattern: ghrunner_systemctl_line)
}
3 changes: 2 additions & 1 deletion dag/test/claim/devboot_subject_identity_witness_test.dag
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module test.claim.devboot_subject_identity_witness

import gunbc.roadmap_dashboard_instance { srv1_live_dashboard_instance }
import std.types { Bool, Int, List, NonEmptyStr, String }
import std.measure { byte_size }
import std.content_hash {
Expand Down Expand Up @@ -734,7 +735,7 @@ fn devboot_ensured_step_projects_layout(acc: Int, step: DeploymentDependencyStep

test fn the_devboot_store_root_is_an_ensured_host_resource_projecting_the_layout_row() -> Bool {
fold(
deployment_ensured_steps(target: deployment_srv1_target()),
deployment_ensured_steps(instance: srv1_live_dashboard_instance(), target: deployment_srv1_target()),
init: 0,
f: (acc, step) => devboot_ensured_step_projects_layout(acc: acc, step: step),
) == 1
Expand Down
10 changes: 10 additions & 0 deletions dag/test/claim/host/host_axis_caps_witness_test.dag
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module test.claim.host_axis_caps

import extdeps.systemd.systemctl { systemctl_revert_memory_caps_argv }
import std.logic { Bool }
import std.types { NonEmptyStr, String, List, list_length }
import gunbc.host_axis_caps {
Expand Down Expand Up @@ -261,3 +262,12 @@ test fn witness_cap_upsert_script_carries_no_hand_spelled_printf_quoting() -> Bo
!string_contains(s: cap_upsert_script(m: apostrophe_cap_member), pattern: "printf '%s' '")
&& !string_contains(s: deploy_runner_memory_cap_author_script(), pattern: "printf '%s' '")
}

// `systemctl revert` TAKES UNIT NAMES ONLY (extdeps.systemd.systemctl
// systemctl_revert_memory_caps_argv). The earlier argv appended "MemoryMax" "MemoryHigh", which
// systemctl read as two more units and reverted silently with exit 0 (observed, systemd 255). Every
// token after the verb must be the unit itself.
test fn the_revert_argv_names_only_the_unit() -> Bool {
join(systemctl_revert_memory_caps_argv(unit: "actions-runner@srv1-01.service"), " ")
== "systemctl revert actions-runner@srv1-01.service"
}
Loading
Loading