swarmctl setup: an existing ruth keeps her permissions
declare_ruth stored the default set plus manage_root_agent over whatever ruth held, so a rerun dropped permissions granted since. It now reads her record and adds manage_root_agent only when it is missing, and writes nothing when she holds it. A new ruth still gets the defaults plus manage_root_agent.
This commit is contained in:
parent
44caa72c19
commit
526cfbec1b
2 changed files with 137 additions and 8 deletions
|
|
@ -43,6 +43,18 @@ pub(crate) fn get(target: &Target, agent: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// The stored record of `agent`, printing nothing.
|
||||
pub(crate) fn fetch(target: &Target, agent: &str) -> Result<Permissions> {
|
||||
let uri = member(agent)?;
|
||||
runtime()?.block_on(send(
|
||||
target,
|
||||
hyper::Method::GET,
|
||||
&uri,
|
||||
None::<&()>,
|
||||
"permissions-get",
|
||||
))
|
||||
}
|
||||
|
||||
/// Run `swarmctl agent permissions set`: store the given permissions, which
|
||||
/// replace the agent's record whole.
|
||||
pub(crate) fn set(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use std::time::{Duration, Instant};
|
|||
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use hive_types::Permission;
|
||||
use swarm_controller_api::permissions::Permissions;
|
||||
use swarm_wire::wanted::AgentState;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
|
|
@ -199,6 +200,44 @@ fn retry<T>(attempts: usize, every: Duration, mut f: impl FnMut() -> Result<T>)
|
|||
Err(last.unwrap_or_else(|| anyhow!("no attempt was made")))
|
||||
}
|
||||
|
||||
/// Gives ruth `manage_root_agent` without taking anything else from her.
|
||||
///
|
||||
/// A new ruth gets the default set plus `manage_root_agent`. An existing one
|
||||
/// keeps what the swarm stored for her and gains `manage_root_agent` only if
|
||||
/// it is missing; one that holds it already is not written at all. Both calls
|
||||
/// retry, since the controller declares a new agent in the background.
|
||||
fn ensure_manager_permission(
|
||||
exists: bool,
|
||||
fetch: impl FnMut() -> Result<Permissions>,
|
||||
mut store: impl FnMut(&[String]) -> Result<()>,
|
||||
every: Duration,
|
||||
) -> Result<()> {
|
||||
let stored = if exists {
|
||||
retry(30, every, fetch)?.permissions
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(granted) = with_manage_root_agent(stored) else {
|
||||
return Ok(());
|
||||
};
|
||||
let names: Vec<String> = granted
|
||||
.iter()
|
||||
.map(|p| <&str>::from(*p).to_owned())
|
||||
.collect();
|
||||
retry(30, every, || store(&names))
|
||||
}
|
||||
|
||||
/// `stored` plus `manage_root_agent`, or `None` when it holds it already. An
|
||||
/// unset record means the default set.
|
||||
fn with_manage_root_agent(stored: Option<Vec<Permission>>) -> Option<Vec<Permission>> {
|
||||
let mut granted = stored.unwrap_or_else(|| Permission::DEFAULT.to_vec());
|
||||
if granted.contains(&Permission::ManageRootAgent) {
|
||||
return None;
|
||||
}
|
||||
granted.push(Permission::ManageRootAgent);
|
||||
Some(granted)
|
||||
}
|
||||
|
||||
/// Whether `program` is an executable file in a `PATH` directory.
|
||||
fn on_path(program: &str) -> bool {
|
||||
std::env::var_os("PATH").is_some_and(|path| on_path_in(program, &path))
|
||||
|
|
@ -284,15 +323,13 @@ impl Actions for Live {
|
|||
if !exists {
|
||||
agent::request_create(target, RUTH, &hive, None)?;
|
||||
}
|
||||
let granted: Vec<String> = Permission::DEFAULT
|
||||
.iter()
|
||||
.chain([&Permission::ManageRootAgent])
|
||||
.map(|p| <&str>::from(*p).to_owned())
|
||||
.collect();
|
||||
let every = Duration::from_secs(2);
|
||||
retry(30, every, || {
|
||||
permissions::store(target, RUTH, &granted, None)
|
||||
})?;
|
||||
ensure_manager_permission(
|
||||
exists,
|
||||
|| permissions::fetch(target, RUTH),
|
||||
|granted| permissions::store(target, RUTH, granted, None).map(drop),
|
||||
every,
|
||||
)?;
|
||||
retry(30, every, || {
|
||||
agent::request_state(target, RUTH, &hive, AgentState::Up)
|
||||
})?;
|
||||
|
|
@ -389,6 +426,86 @@ mod tests {
|
|||
assert_eq!((calls, err.to_string().as_str()), (4, "never"));
|
||||
}
|
||||
|
||||
fn stored(permissions: Option<Vec<Permission>>) -> Permissions {
|
||||
Permissions {
|
||||
permissions,
|
||||
..Permissions::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the ruth step against `record`; returns what it stored, if anything.
|
||||
fn ruth_step(exists: bool, record: &Permissions) -> (Option<Vec<String>>, usize) {
|
||||
let mut written = None;
|
||||
let mut fetches = 0;
|
||||
ensure_manager_permission(
|
||||
exists,
|
||||
|| {
|
||||
fetches += 1;
|
||||
Ok(record.clone())
|
||||
},
|
||||
|granted| {
|
||||
written = Some(granted.to_vec());
|
||||
Ok(())
|
||||
},
|
||||
Duration::ZERO,
|
||||
)
|
||||
.expect("the step succeeds");
|
||||
(written, fetches)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_existing_ruth_keeps_her_permissions_and_gains_manage_root_agent() {
|
||||
let held = vec![
|
||||
Permission::Messaging,
|
||||
Permission::Logs,
|
||||
Permission::WebTools,
|
||||
];
|
||||
let (written, _) = ruth_step(true, &stored(Some(held)));
|
||||
assert_eq!(
|
||||
written.expect("manage_root_agent was missing, so it is written"),
|
||||
["messaging", "logs", "web_tools", "manage_root_agent"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_existing_ruth_that_holds_manage_root_agent_is_not_written() {
|
||||
let held = vec![Permission::Logs, Permission::ManageRootAgent];
|
||||
let (written, fetches) = ruth_step(true, &stored(Some(held)));
|
||||
assert_eq!(written, None);
|
||||
assert_eq!(fetches, 1, "her record was read, not assumed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_ruth_gets_the_defaults_and_manage_root_agent_without_a_read() {
|
||||
let (written, fetches) = ruth_step(false, &stored(None));
|
||||
assert_eq!(
|
||||
written.expect("written"),
|
||||
[
|
||||
"messaging",
|
||||
"meta",
|
||||
"inbox",
|
||||
"execution",
|
||||
"manage_root_agent"
|
||||
]
|
||||
);
|
||||
assert_eq!(fetches, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_existing_ruth_on_the_default_set_gets_the_defaults_and_manage_root_agent() {
|
||||
let (written, _) = ruth_step(true, &stored(None));
|
||||
assert_eq!(
|
||||
written.expect("written"),
|
||||
[
|
||||
"messaging",
|
||||
"meta",
|
||||
"inbox",
|
||||
"execution",
|
||||
"manage_root_agent"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_program_on_path_is_found_only_when_it_is_executable() {
|
||||
let dir = std::env::temp_dir().join(format!("swarmctl-onpath-{}", std::process::id()));
|
||||
|
|
|
|||
Loading…
Reference in a new issue