Add smooth node enrollment flow

This commit is contained in:
Eric Wendland 2026-05-21 18:01:38 +02:00
commit 27a79768e4
12 changed files with 1662 additions and 22 deletions

View file

@ -139,6 +139,10 @@ pub enum NodeCommand {
Id,
Status,
List,
Enroll {
#[command(subcommand)]
command: NodeEnrollCommand,
},
Rename {
node: String,
name: String,
@ -163,6 +167,48 @@ pub enum NodeCommand {
},
}
#[derive(Debug, Subcommand)]
pub enum NodeEnrollCommand {
Request {
#[arg(long)]
node_name: String,
#[arg(long = "capability")]
capabilities: Vec<String>,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
out: Option<PathBuf>,
},
Submit {
owner_node: String,
#[arg(long)]
request_id: Option<String>,
#[arg(long)]
path: Option<PathBuf>,
},
Import {
path: PathBuf,
},
List {
#[arg(long)]
status: Option<String>,
},
Approve {
request_id: String,
#[arg(long)]
signing_key: PathBuf,
#[arg(long)]
admin_key: Option<PathBuf>,
#[arg(long)]
node_name: Option<String>,
#[arg(long = "capability")]
capabilities: Vec<String>,
},
Sync {
owner_node: String,
},
}
#[derive(Debug, Subcommand)]
pub enum PeerCommand {
Export {
@ -210,6 +256,9 @@ pub enum AuthCommand {
resource: String,
capability: String,
},
Sync {
node: String,
},
Grant {
subject: String,
resource: String,
@ -757,6 +806,46 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
Command::Node {
command: NodeCommand::List,
} => ControlRequest::NodeList,
Command::Node {
command: NodeCommand::Enroll { command },
} => match command {
NodeEnrollCommand::Request {
node_name,
capabilities,
reason,
out,
} => ControlRequest::NodeEnrollRequest {
node_name,
capabilities,
reason,
out,
},
NodeEnrollCommand::Submit {
owner_node,
request_id,
path,
} => ControlRequest::NodeEnrollSubmit {
owner_node,
request_id,
path,
},
NodeEnrollCommand::Import { path } => ControlRequest::NodeEnrollImport { path },
NodeEnrollCommand::List { status } => ControlRequest::NodeEnrollList { status },
NodeEnrollCommand::Approve {
request_id,
signing_key,
admin_key,
node_name,
capabilities,
} => ControlRequest::NodeEnrollApprove {
request_id,
signing_key_path: signing_key,
admin_key_path: admin_key,
node_name,
capabilities,
},
NodeEnrollCommand::Sync { owner_node } => ControlRequest::NodeEnrollSync { owner_node },
},
Command::Node {
command:
NodeCommand::Rename {
@ -829,6 +918,9 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
Command::Keychain {
command: KeychainCommand::Sync { node },
} => ControlRequest::KeychainSync { node },
Command::Auth {
command: AuthCommand::Sync { node },
} => ControlRequest::AuthSync { node },
Command::Auth {
command:
AuthCommand::Explain {
@ -1687,6 +1779,23 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
println!("invalid_ops_rejected: {invalid_ops_rejected}");
println!("note: {note}");
}
ControlResponse::AuthSynced {
peer_node_id,
peer_agent_id,
endpoint_id,
ops_imported,
signatures_imported,
invalid_ops_rejected,
note,
} => {
println!("synced auth from: {peer_node_id}");
println!("agent: {peer_agent_id}");
println!("endpoint: {endpoint_id}");
println!("ops_imported: {ops_imported}");
println!("signatures_imported: {signatures_imported}");
println!("invalid_ops_rejected: {invalid_ops_rejected}");
println!("note: {note}");
}
ControlResponse::SecretStatus { secrets } => {
if secrets.is_empty() {
println!("no resource secrets");
@ -1832,9 +1941,96 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
}
println!("note: {note}");
}
ControlResponse::NodeGrantUpdated { op, note } => {
ControlResponse::NodeGrantUpdated {
op,
signatures,
note,
} => {
println!("recorded auth op: {}", op.id);
println!("resource: {}", op.resource);
for signature in signatures {
println!(
"signed auth op: {} by {} ({})",
signature.op_id, signature.signer, signature.namespace
);
}
println!("note: {note}");
}
ControlResponse::NodeEnrollmentRequested { request, out, note } => {
println!("node enrollment request: {}", request.id);
println!("node: {}", request.requester_node);
println!("requested_name: {}", request.requested_node_name);
println!("status: {}", request.status);
if let Some(out) = out {
println!("written: {}", out.display());
}
println!("note: {note}");
}
ControlResponse::NodeEnrollmentSubmitted {
request_id,
owner_node_id,
accepted,
note,
} => {
println!("submitted enrollment request: {request_id}");
println!("owner_node: {owner_node_id}");
println!("accepted: {accepted}");
println!("note: {note}");
}
ControlResponse::NodeEnrollmentImported { request, note } => {
println!("imported enrollment request: {}", request.id);
println!("node: {}", request.requester_node);
println!("requested_name: {}", request.requested_node_name);
println!("status: {}", request.status);
println!("note: {note}");
}
ControlResponse::NodeEnrollmentList { requests, note } => {
if requests.is_empty() {
println!("no node enrollment requests");
} else {
for request in requests {
println!(
"{}\t{}\t{}\t{} capabilities",
request.id,
request.status,
request.requested_node_name,
request.requested_capabilities.len()
);
}
}
println!("note: {note}");
}
ControlResponse::NodeEnrollmentApproved {
request,
keychain_ops,
keychain_signatures,
auth_ops,
auth_signatures,
note,
} => {
println!("approved enrollment request: {}", request.id);
println!("node: {}", request.requester_node);
println!("keychain_ops: {}", keychain_ops.len());
println!("keychain_signatures: {}", keychain_signatures.len());
println!("auth_ops: {}", auth_ops.len());
println!("auth_signatures: {}", auth_signatures.len());
println!("note: {note}");
}
ControlResponse::NodeEnrollmentSynced {
owner_node,
keychain_ops_imported,
keychain_signatures_imported,
auth_ops_imported,
auth_signatures_imported,
invalid_ops_rejected,
note,
} => {
println!("synced enrollment from: {owner_node}");
println!("keychain_ops_imported: {keychain_ops_imported}");
println!("keychain_signatures_imported: {keychain_signatures_imported}");
println!("auth_ops_imported: {auth_ops_imported}");
println!("auth_signatures_imported: {auth_signatures_imported}");
println!("invalid_ops_rejected: {invalid_ops_rejected}");
println!("note: {note}");
}
ControlResponse::SshCertRequested { request } => {