swarmctl: interactive setup walks the first-run ceremony
swarmctl setup with no subcommand checks the setup status first, then takes the open steps one at a time: prompts for input, shows a running action as one live line, prints the store keys, root token and new password under a warning until the operator confirms, and names the deploy option for a step this host cannot do. It calls the code of bao init, bao grant-bootstrap, user add, forge make-admin, agent create, agent permissions set and agent set-state. Refuses without root or a terminal.
This commit is contained in:
parent
4fae6a7766
commit
44caa72c19
1 changed files with 2172 additions and 82 deletions
|
|
@ -20,7 +20,9 @@
|
|||
//! reach it over its unix socket on its host, or, with `--swarm`, through
|
||||
//! the swarm domain's gateway as the operator's own SSO account
|
||||
//! ([`remote`]). The `user` verbs act on the local users file only; the `bao`
|
||||
//! verbs run the `bao` CLI against the secret store.
|
||||
//! verbs run the `bao` CLI against the secret store. `setup` walks the first-run
|
||||
//! ceremony interactively by calling the code of those verbs ([`interactive`],
|
||||
//! [`terminal`]).
|
||||
//!
|
||||
//! Distinct from `hivectl`, which drives one hive's `hive-c0re` over its
|
||||
//! admin socket. This crate deliberately does not link `swarm-controller`,
|
||||
|
|
@ -31,11 +33,13 @@ mod agents_md;
|
|||
mod bao;
|
||||
mod forge;
|
||||
mod inference;
|
||||
mod interactive;
|
||||
mod login;
|
||||
mod permissions;
|
||||
mod remote;
|
||||
mod schedule;
|
||||
mod setup;
|
||||
mod terminal;
|
||||
mod users;
|
||||
mod watch;
|
||||
|
||||
|
|
@ -75,7 +79,7 @@ const LOGIN_SWARM_ENVS: &[&str] = &["SWARMCTL_SWARM", "SWARMCTL_LOGIN_DEFAULT_SW
|
|||
#[derive(Args)]
|
||||
struct RemoteArgs {
|
||||
/// The swarm's domain, as `https://<domain>` or the bare domain. Given,
|
||||
/// the `agent`, `schedule` and `forge` verbs, and the agent checks of `setup status`, go through its gateway as the
|
||||
/// the `agent`, `schedule` and `forge` verbs, and the agent checks and steps of `setup`, go through its gateway as the
|
||||
/// account `swarmctl login` logged in, instead of over the controller's
|
||||
/// socket. Falls back to `SWARMCTL_SWARM`; `login` then falls back to
|
||||
/// `SWARMCTL_LOGIN_DEFAULT_SWARM`, which the swarm-controller nix module
|
||||
|
|
@ -217,11 +221,20 @@ enum Verb {
|
|||
#[command(subcommand)]
|
||||
command: ForgeVerb,
|
||||
},
|
||||
/// Show where the first-run setup ceremony stands.
|
||||
Setup {
|
||||
#[command(subcommand)]
|
||||
command: SetupVerb,
|
||||
},
|
||||
/// Walk through the first-run setup, interactively.
|
||||
///
|
||||
/// Run it as root, in a terminal. It checks `docs/1-getting-started/setup.md`
|
||||
/// first, so a rerun continues at the first open step, then takes the open
|
||||
/// steps one at a time. It prompts for what a step needs, shows a running
|
||||
/// action as one live line, and prints anything to record in colour under a
|
||||
/// ⚠️ warning, such as the secret store's keys and root token. It prints those
|
||||
/// once and stores none of them. A step that needs a service this host
|
||||
/// doesn't run names the deploy option to enable and the host to run
|
||||
/// `swarmctl setup` on. A step without a verb yet prints its command and
|
||||
/// waits until you confirm you ran it.
|
||||
///
|
||||
/// For scripts, use `setup status` and the individual verbs.
|
||||
Setup(SetupArgs),
|
||||
/// Initialise the swarm's secret store and bootstrap its granter.
|
||||
///
|
||||
/// Runs the `bao` CLI against the store over HTTPS, so the store need
|
||||
|
|
@ -271,6 +284,15 @@ enum Verb {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(args_conflicts_with_subcommands = true)]
|
||||
struct SetupArgs {
|
||||
#[command(subcommand)]
|
||||
command: Option<SetupVerb>,
|
||||
#[command(flatten)]
|
||||
socket: ControllerSocketArg,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum SetupVerb {
|
||||
/// Print each step of the first-run setup, in order, as `done`, `not done`
|
||||
|
|
@ -837,20 +859,7 @@ fn main() -> Result<()> {
|
|||
} => bao::grant_bootstrap(args),
|
||||
// Same socket-resolution reasoning as `Create` above.
|
||||
Verb::Schedule { command } => schedule(&remote, command),
|
||||
// Neither the controller socket nor the users file is required: a
|
||||
// host without one reports the steps that need the other.
|
||||
Verb::Setup {
|
||||
command: SetupVerb::Status(args),
|
||||
} => {
|
||||
let target = args.socket.resolve(&remote);
|
||||
let users_file = path_from(paths.users_file, "SWARMCTL_AUTHELIA_USERS_FILE");
|
||||
let complete = setup::status(target, users_file);
|
||||
io::stdout().flush().context("writing the report")?;
|
||||
if !complete {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Verb::Setup(args) => setup_verb(paths, &remote, args),
|
||||
Verb::Login => login::login(
|
||||
&remote
|
||||
.login_swarm()?
|
||||
|
|
@ -895,6 +904,28 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Neither the controller socket nor the users file is required: a host
|
||||
/// without one still reports, and walks, the steps that need neither.
|
||||
fn setup_verb(paths: PathArgs, remote: &RemoteArgs, args: SetupArgs) -> Result<()> {
|
||||
let users_file = path_from(paths.users_file, "SWARMCTL_AUTHELIA_USERS_FILE");
|
||||
match args.command {
|
||||
Some(SetupVerb::Status(status)) => {
|
||||
let target = status.socket.resolve(remote);
|
||||
let complete = setup::status(&target, &users_file);
|
||||
io::stdout().flush().context("writing the report")?;
|
||||
if !complete {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
let target = args.socket.resolve(remote);
|
||||
let authelia_bin = path_from(paths.authelia_bin, "SWARMCTL_AUTHELIA_BIN");
|
||||
terminal::setup(target, users_file, authelia_bin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inference(remote: &RemoteArgs, command: InferenceVerb) -> Result<()> {
|
||||
match command {
|
||||
InferenceVerb::Get(a) => inference::get(&a.socket.resolve(remote)?, &a.agent),
|
||||
|
|
@ -966,6 +997,16 @@ fn schedule(remote: &RemoteArgs, command: ScheduleVerb) -> Result<()> {
|
|||
}
|
||||
|
||||
fn user_add(paths: &Paths, args: AddArgs) -> Result<()> {
|
||||
let username = args.username.clone();
|
||||
let password = add_user(paths, args)?;
|
||||
println!("added {username} to {}", paths.users_file.display());
|
||||
println!("password: {}", *password);
|
||||
println!("this password is stored nowhere — record it now");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`user_add`] without its printing: returns the generated password.
|
||||
fn add_user(paths: &Paths, args: AddArgs) -> Result<zeroize::Zeroizing<String>> {
|
||||
users::validate_username(&args.username)?;
|
||||
|
||||
let _lock = lock_users_file(&paths.users_file)?;
|
||||
|
|
@ -992,11 +1033,7 @@ fn user_add(paths: &Paths, args: AddArgs) -> Result<()> {
|
|||
);
|
||||
|
||||
publish(paths, &mut store)?;
|
||||
|
||||
println!("added {} to {}", args.username, paths.users_file.display());
|
||||
println!("password: {}", generated.password);
|
||||
println!("this password is stored nowhere — record it now");
|
||||
Ok(())
|
||||
Ok(zeroize::Zeroizing::new(generated.password))
|
||||
}
|
||||
|
||||
fn user_reset_password(paths: &Paths, args: &ResetPasswordArgs) -> Result<()> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue