Public dry run dryrun-0b6a82183be2

Source commit: 0b6a82183be25780b308c75442a9b5f602f727d4
This commit is contained in:
Michel Paulissen 2026-07-03 19:11:24 +02:00
parent 8c0c336ae9
commit 5386bb851f
16 changed files with 608 additions and 95 deletions

View file

@ -157,6 +157,8 @@ enum BundleCommands {
#[derive(Clone, Debug, Parser)]
struct AgentEnrollArgs {
#[arg(long)]
json: bool,
#[arg(long = "public-key")]
public_key: String,
}
@ -231,6 +233,8 @@ struct ProjectSelectArgs {
struct BundleInspectArgs {
#[arg(long)]
project: Option<PathBuf>,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, Parser)]
@ -239,6 +243,8 @@ struct BuildArgs {
project: Option<PathBuf>,
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, Parser)]
@ -250,6 +256,8 @@ struct RunArgs {
coordinator: Option<String>,
#[arg(long)]
local: bool,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, Subcommand)]
@ -277,6 +285,8 @@ struct AttachArgs {
enrollment_grant: Option<String>,
#[arg(long = "public-key")]
public_key: Option<String>,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, Parser)]
@ -414,6 +424,8 @@ struct ArtifactExportArgs {
struct DapArgs {
#[arg(long)]
plan: bool,
#[arg(long)]
json: bool,
#[arg(last = true)]
args: Vec<String>,
}
@ -796,6 +808,7 @@ fn project_status_report(args: ProjectStatusArgs, cwd: PathBuf) -> Result<Value>
let inspection = bundle_inspection(
BundleInspectArgs {
project: Some(cwd.clone()),
json: true,
},
cwd.clone(),
)
@ -864,6 +877,7 @@ fn build_report(args: BuildArgs, cwd: PathBuf) -> Result<Value> {
let inspection = bundle_inspection(
BundleInspectArgs {
project: args.project.clone(),
json: true,
},
cwd,
)?;
@ -1247,137 +1261,428 @@ fn admin_suspend_tenant_report(args: AdminSuspendTenantArgs) -> Result<Value> {
}))
}
fn emit_report<T: Serialize>(report: &T, json_output: bool) -> Result<()> {
let value = serde_json::to_value(report)?;
if json_output {
println!("{}", serde_json::to_string_pretty(&value)?);
} else {
println!("{}", human_report(&value));
}
Ok(())
}
fn human_report(value: &Value) -> String {
let mut lines = Vec::new();
let title = value
.get("command")
.and_then(Value::as_str)
.map(|command| format!("Disasmer {command}"))
.or_else(|| {
if value.get("human_flow").is_some() {
Some("Disasmer login".to_owned())
} else if value.get("metadata").is_some()
&& value.get("source_provider_manifest").is_some()
{
Some("Disasmer bundle inspect".to_owned())
} else if value.get("entry").is_some() && value.get("session").is_some() {
Some("Disasmer run".to_owned())
} else if value.get("capabilities").is_some() && value.get("node").is_some() {
Some("Disasmer node attach".to_owned())
} else if value.get("public_key_fingerprint").is_some() {
Some("Disasmer agent enroll".to_owned())
} else {
None
}
})
.unwrap_or_else(|| "Disasmer report".to_owned());
lines.push(title);
push_string_field(&mut lines, value, "status", "status");
push_string_field(&mut lines, value, "coordinator", "coordinator");
push_string_field(&mut lines, value, "active_coordinator", "coordinator");
push_string_field(&mut lines, value, "tenant", "tenant");
push_string_field(&mut lines, value, "project", "project");
push_string_field(&mut lines, value, "process", "process");
push_string_field(&mut lines, value, "node", "node");
push_string_field(&mut lines, value, "artifact", "artifact");
push_string_field(&mut lines, value, "entry", "entry");
push_string_field(&mut lines, value, "config_file", "config");
push_string_field(&mut lines, value, "adapter", "adapter");
if let Some(project) = value.get("project").and_then(Value::as_str) {
if !lines
.iter()
.any(|line| line == &format!("project: {project}"))
{
lines.push(format!("project: {project}"));
}
}
if let Some(project_config) = value.get("project_config").or_else(|| value.get("project")) {
if project_config.is_object() {
push_nested_string_field(&mut lines, project_config, "tenant", "tenant");
push_nested_string_field(&mut lines, project_config, "project", "project");
push_nested_string_field(&mut lines, project_config, "coordinator", "coordinator");
}
}
if let Some(metadata) = value.get("metadata") {
push_nested_string_field(&mut lines, metadata, "identity", "bundle");
if let Some(environments) = metadata.get("environments").and_then(Value::as_array) {
let names = environments
.iter()
.filter_map(|env| env.get("name").and_then(Value::as_str))
.collect::<Vec<_>>();
if !names.is_empty() {
lines.push(format!("environments: {}", names.join(", ")));
}
}
}
if let Some(bundle) = value.get("bundle") {
if let Some(metadata) = bundle.get("metadata") {
push_nested_string_field(&mut lines, metadata, "identity", "bundle");
}
}
if let Some(flow) = value.get("human_flow") {
if let Some(device) = flow.get("Device") {
lines.push("flow: device".to_owned());
push_nested_string_field(&mut lines, device, "verification_url", "verify");
push_nested_string_field(&mut lines, device, "user_code", "code");
} else if let Some(browser) = flow.get("Browser") {
lines.push("flow: browser".to_owned());
push_nested_string_field(&mut lines, browser, "authorization_url", "open");
push_nested_string_field(&mut lines, browser, "callback_path", "callback");
}
}
if let Some(session) = value.get("session") {
lines.push(format!("session: {}", compact_json(session)));
}
if let Some(coordinator_selection) = value.get("coordinator") {
if coordinator_selection.is_object() {
lines.push(format!(
"coordinator: {}",
compact_json(coordinator_selection)
));
}
}
if let Some(response) = value
.get("response")
.or_else(|| value.get("coordinator_response"))
{
if let Some(response_type) = response.get("type").and_then(Value::as_str) {
lines.push(format!("coordinator response: {response_type}"));
}
}
if let Some(events) = value
.pointer("/events/response/events")
.and_then(Value::as_array)
{
lines.push(format!("events: {}", events.len()));
}
if let Some(events) = value
.pointer("/coordinator_response/response/events")
.and_then(Value::as_array)
{
lines.push(format!("events: {}", events.len()));
}
if let Some(requires_confirmation) = value.get("requires_confirmation").and_then(Value::as_bool)
{
lines.push(format!(
"confirmation: {}",
if requires_confirmation {
"required"
} else {
"confirmed"
}
));
}
if let Some(flag) = value
.get("private_website_required")
.and_then(Value::as_bool)
{
lines.push(format!("private website required: {flag}"));
}
if let Some(next_actions) = value.get("next_actions").and_then(Value::as_array) {
let actions = next_actions
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
if !actions.is_empty() {
lines.push(format!("next: {}", actions.join("; ")));
}
}
if let Some(auth) = value.get("auth").or_else(|| value.get("session")) {
if let Some(kind) = auth.get("kind").and_then(Value::as_str) {
lines.push(format!("auth: {kind}"));
}
if let Some(authenticated) = auth.get("authenticated").and_then(Value::as_bool) {
lines.push(format!("authenticated: {authenticated}"));
}
if let Some(expires) = auth.get("expires_at").and_then(Value::as_str) {
lines.push(format!("token expiry: {expires}"));
} else if let Some(posture) = auth.get("token_expiry_posture").and_then(Value::as_str) {
lines.push(format!("token expiry: {posture}"));
} else if auth.get("authenticated").is_some() {
lines.push("token expiry: unavailable".to_owned());
}
}
if let Some(dependencies) = value.get("dependencies").and_then(Value::as_object) {
let mut entries = dependencies
.iter()
.map(|(name, available)| {
format!(
"{name}={}",
if available.as_bool().unwrap_or(false) {
"ok"
} else {
"missing"
}
)
})
.collect::<Vec<_>>();
entries.sort();
if !entries.is_empty() {
lines.push(format!("dependencies: {}", entries.join(", ")));
}
}
if let Some(readiness) = value.get("node_readiness") {
push_nested_string_field(&mut lines, readiness, "os", "node os");
push_nested_string_field(&mut lines, readiness, "arch", "node arch");
if let Some(capabilities) = readiness.get("capabilities").and_then(Value::as_array) {
let caps = capabilities
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
if !caps.is_empty() {
lines.push(format!("node capabilities: {}", caps.join(", ")));
}
}
}
lines.dedup();
lines.join("\n")
}
fn push_string_field(lines: &mut Vec<String>, value: &Value, key: &str, label: &str) {
if let Some(text) = value.get(key).and_then(Value::as_str) {
lines.push(format!("{label}: {text}"));
} else if let Some(path) = value.get(key).and_then(|value| {
value
.as_object()
.and_then(|object| object.get("display"))
.and_then(Value::as_str)
}) {
lines.push(format!("{label}: {path}"));
}
}
fn push_nested_string_field(lines: &mut Vec<String>, value: &Value, key: &str, label: &str) {
if let Some(text) = value.get(key).and_then(Value::as_str) {
lines.push(format!("{label}: {text}"));
}
}
fn compact_json(value: &Value) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "<unprintable>".to_owned())
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Doctor(args) => {
println!(
"{}",
serde_json::to_string_pretty(&doctor_report(args, std::env::current_dir()?)?)?
);
let json_output = args.scope.json;
let report = doctor_report(args, std::env::current_dir()?)?;
emit_report(&report, json_output)?;
}
Commands::Login(args) => {
let json_output = args.json;
if args.complete_browser_code.is_some() {
let report = execute_browser_login_completion(args)?;
println!("{}", serde_json::to_string_pretty(&report)?);
emit_report(&report, json_output)?;
} else if args.browser && !args.plan {
let json_output = args.json;
let report = execute_interactive_browser_login(args)?;
if json_output {
println!("{}", serde_json::to_string_pretty(&report)?);
emit_report(&report, true)?;
} else {
print_browser_login_success(&report);
}
} else {
let plan = login_plan(args);
println!("{}", serde_json::to_string_pretty(&plan)?);
emit_report(&plan, json_output)?;
}
}
Commands::Auth { command } => {
let report = match command {
AuthCommands::Status(args) => auth_status_report(args, std::env::current_dir()?),
AuthCommands::Logout(args) => auth_logout_report(args, std::env::current_dir()?),
}?;
println!("{}", serde_json::to_string_pretty(&report)?);
let (report, json_output) = match command {
AuthCommands::Status(args) => {
let json_output = args.scope.json;
(
auth_status_report(args, std::env::current_dir()?)?,
json_output,
)
}
AuthCommands::Logout(args) => {
let json_output = args.scope.json;
(
auth_logout_report(args, std::env::current_dir()?)?,
json_output,
)
}
};
emit_report(&report, json_output)?;
}
Commands::Agent {
command: AgentCommands::Enroll(args),
} => {
let json_output = args.json;
let plan = agent_enrollment_plan(args);
println!("{}", serde_json::to_string_pretty(&plan)?);
emit_report(&plan, json_output)?;
}
Commands::Key { command } => {
let report = match command {
KeyCommands::Add(args) => key_add_report(args),
KeyCommands::List(args) => key_list_report(args),
KeyCommands::Revoke(args) => key_revoke_report(args),
}?;
println!("{}", serde_json::to_string_pretty(&report)?);
let (report, json_output) = match command {
KeyCommands::Add(args) => {
let json_output = args.scope.json;
(key_add_report(args)?, json_output)
}
KeyCommands::List(args) => {
let json_output = args.scope.json;
(key_list_report(args)?, json_output)
}
KeyCommands::Revoke(args) => {
let json_output = args.scope.json;
(key_revoke_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Project { command } => {
let cwd = std::env::current_dir()?;
let report = match command {
ProjectCommands::Init(args) => project_init_report(args, cwd),
ProjectCommands::Status(args) => project_status_report(args, cwd),
ProjectCommands::List(args) => project_list_report(args, cwd),
ProjectCommands::Select(args) => project_select_report(args, cwd),
}?;
println!("{}", serde_json::to_string_pretty(&report)?);
let (report, json_output) = match command {
ProjectCommands::Init(args) => {
let json_output = args.scope.json;
(project_init_report(args, cwd)?, json_output)
}
ProjectCommands::Status(args) => {
let json_output = args.scope.json;
(project_status_report(args, cwd)?, json_output)
}
ProjectCommands::List(args) => {
let json_output = args.scope.json;
(project_list_report(args, cwd)?, json_output)
}
ProjectCommands::Select(args) => {
let json_output = args.scope.json;
(project_select_report(args, cwd)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Inspect(args) => {
let json_output = args.json;
let inspection = bundle_inspection(args, std::env::current_dir()?)?;
println!("{}", serde_json::to_string_pretty(&inspection)?);
emit_report(&inspection, json_output)?;
}
Commands::Build(args) => {
let json_output = args.json;
let report = build_report(args, std::env::current_dir()?)?;
println!("{}", serde_json::to_string_pretty(&report)?);
emit_report(&report, json_output)?;
}
Commands::Bundle {
command: BundleCommands::Inspect(args),
} => {
let json_output = args.json;
let inspection = bundle_inspection(args, std::env::current_dir()?)?;
println!("{}", serde_json::to_string_pretty(&inspection)?);
emit_report(&inspection, json_output)?;
}
Commands::Run(args) => {
let json_output = args.json;
let plan = run_plan(args, std::env::current_dir()?, session_from_env())?;
if should_execute_local_node(&plan) {
let report = execute_local_node_run(plan)?;
println!("{}", serde_json::to_string_pretty(&report)?);
emit_report(&report, json_output)?;
} else {
println!("{}", serde_json::to_string_pretty(&plan)?);
emit_report(&plan, json_output)?;
}
}
Commands::Node {
command: NodeCommands::Attach(args),
} => {
let json_output = args.json;
if args.coordinator.is_some() {
let report = execute_node_attach(args)?;
println!("{}", serde_json::to_string_pretty(&report)?);
emit_report(&report, json_output)?;
} else {
let plan = attach_plan(args);
println!("{}", serde_json::to_string_pretty(&plan)?);
emit_report(&plan, json_output)?;
}
}
Commands::Node { command } => {
let report = match command {
NodeCommands::Enroll(args) => node_enroll_report(args),
NodeCommands::List(args) => node_list_report(args),
NodeCommands::Status(args) => node_status_report(args),
NodeCommands::Revoke(args) => node_revoke_report(args),
let (report, json_output) = match command {
NodeCommands::Enroll(args) => {
let json_output = args.scope.json;
(node_enroll_report(args)?, json_output)
}
NodeCommands::List(args) => {
let json_output = args.scope.json;
(node_list_report(args)?, json_output)
}
NodeCommands::Status(args) => {
let json_output = args.scope.json;
(node_status_report(args)?, json_output)
}
NodeCommands::Revoke(args) => {
let json_output = args.scope.json;
(node_revoke_report(args)?, json_output)
}
NodeCommands::Attach(_) => unreachable!("node attach is handled above"),
}?;
println!("{}", serde_json::to_string_pretty(&report)?);
};
emit_report(&report, json_output)?;
}
Commands::Process { command } => {
let report = match command {
ProcessCommands::Status(args) => process_status_report(args),
ProcessCommands::Restart(args) => process_restart_report(args),
ProcessCommands::Cancel(args) => process_cancel_report(args),
}?;
println!("{}", serde_json::to_string_pretty(&report)?);
let (report, json_output) = match command {
ProcessCommands::Status(args) => {
let json_output = args.scope.json;
(process_status_report(args)?, json_output)
}
ProcessCommands::Restart(args) => {
let json_output = args.scope.json;
(process_restart_report(args)?, json_output)
}
ProcessCommands::Cancel(args) => {
let json_output = args.scope.json;
(process_cancel_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Task {
command: TaskCommands::List(args),
} => {
println!(
"{}",
serde_json::to_string_pretty(&task_list_report(args)?)?
);
let json_output = args.scope.json;
emit_report(&task_list_report(args)?, json_output)?;
}
Commands::Logs(args) => {
println!("{}", serde_json::to_string_pretty(&logs_report(args)?)?);
let json_output = args.scope.json;
emit_report(&logs_report(args)?, json_output)?;
}
Commands::Artifact { command } => {
let report = match command {
ArtifactCommands::List(args) => artifact_list_report(args),
ArtifactCommands::Download(args) => artifact_download_report(args),
ArtifactCommands::Export(args) => artifact_export_report(args),
}?;
println!("{}", serde_json::to_string_pretty(&report)?);
let (report, json_output) = match command {
ArtifactCommands::List(args) => {
let json_output = args.scope.json;
(artifact_list_report(args)?, json_output)
}
ArtifactCommands::Download(args) => {
let json_output = args.scope.json;
(artifact_download_report(args)?, json_output)
}
ArtifactCommands::Export(args) => {
let json_output = args.scope.json;
(artifact_export_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Dap(args) => {
let json_output = args.json;
if args.plan {
println!("{}", serde_json::to_string_pretty(&dap_plan(args)?)?);
emit_report(&dap_plan(args)?, json_output)?;
} else {
return exec_dap(args);
}
@ -1385,30 +1690,42 @@ fn main() -> Result<()> {
Commands::Debug {
command: DebugCommands::Attach(args),
} => {
println!(
"{}",
serde_json::to_string_pretty(&debug_attach_report(args)?)?
);
let json_output = args.scope.json;
emit_report(&debug_attach_report(args)?, json_output)?;
}
Commands::Quota {
command: QuotaCommands::Status(args),
} => {
println!(
"{}",
serde_json::to_string_pretty(&quota_status_report(args)?)?
);
let json_output = args.scope.json;
emit_report(&quota_status_report(args)?, json_output)?;
}
Commands::Admin { command } => {
let report = match command {
AdminCommands::Status(args) => admin_status_report(args),
AdminCommands::Bootstrap(args) => {
admin_bootstrap_report(args, std::env::current_dir()?)
let (report, json_output) = match command {
AdminCommands::Status(args) => {
let json_output = args.scope.json;
(admin_status_report(args)?, json_output)
}
AdminCommands::RevokeNode(args) => node_revoke_report(args),
AdminCommands::StopProcess(args) => process_cancel_report(args),
AdminCommands::SuspendTenant(args) => admin_suspend_tenant_report(args),
}?;
println!("{}", serde_json::to_string_pretty(&report)?);
AdminCommands::Bootstrap(args) => {
let json_output = args.scope.json;
(
admin_bootstrap_report(args, std::env::current_dir()?)?,
json_output,
)
}
AdminCommands::RevokeNode(args) => {
let json_output = args.scope.json;
(node_revoke_report(args)?, json_output)
}
AdminCommands::StopProcess(args) => {
let json_output = args.scope.json;
(process_cancel_report(args)?, json_output)
}
AdminCommands::SuspendTenant(args) => {
let json_output = args.scope.json;
(admin_suspend_tenant_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
}
Ok(())
@ -1450,13 +1767,19 @@ fn auth_state_value() -> Value {
"kind": "anonymous",
"authenticated": false,
"source": "environment",
"token_expiry_posture": "no_session",
}),
CliSession::HumanSession => json!({
"kind": "human",
"authenticated": true,
"source": "DISASMER_TOKEN",
"provider_tokens_exposed_to_nodes": false,
}),
CliSession::HumanSession => {
let expires_at = std::env::var("DISASMER_TOKEN_EXPIRES_AT").ok();
json!({
"kind": "human",
"authenticated": true,
"source": "DISASMER_TOKEN",
"provider_tokens_exposed_to_nodes": false,
"expires_at": expires_at,
"token_expiry_posture": if expires_at.is_some() { "expires_at" } else { "unknown_env_token" },
})
}
CliSession::AgentPublicKey {
public_key_fingerprint,
browser_interaction_required,
@ -1466,6 +1789,7 @@ fn auth_state_value() -> Value {
"source": "DISASMER_AGENT_PUBLIC_KEY",
"public_key_fingerprint": public_key_fingerprint,
"browser_interaction_required": browser_interaction_required,
"token_expiry_posture": "not_applicable_public_key",
}),
}
}
@ -2549,6 +2873,7 @@ mod tests {
project: None,
coordinator: None,
local: false,
json: false,
};
let plan = run_plan(
args,
@ -2747,6 +3072,7 @@ mod tests {
let first = bundle_inspection(
BundleInspectArgs {
project: Some(temp.path().to_path_buf()),
json: false,
},
PathBuf::from("/unused"),
)
@ -2759,6 +3085,7 @@ mod tests {
let second = bundle_inspection(
BundleInspectArgs {
project: Some(temp.path().to_path_buf()),
json: false,
},
PathBuf::from("/unused"),
)
@ -2777,6 +3104,7 @@ mod tests {
let first = bundle_inspection(
BundleInspectArgs {
project: Some(first.path().to_path_buf()),
json: false,
},
PathBuf::from("/unused"),
)
@ -2784,6 +3112,7 @@ mod tests {
let second = bundle_inspection(
BundleInspectArgs {
project: Some(second.path().to_path_buf()),
json: false,
},
PathBuf::from("/unused"),
)
@ -2926,6 +3255,71 @@ mod tests {
}
}
#[test]
fn cli_first_json_mode_parses_for_primary_commands() {
for args in [
&["disasmer", "doctor", "--json"][..],
&["disasmer", "login", "--json"],
&["disasmer", "auth", "status", "--json"],
&[
"disasmer",
"agent",
"enroll",
"--public-key",
"key",
"--json",
],
&[
"disasmer",
"key",
"add",
"--agent",
"agent",
"--public-key",
"key",
"--json",
],
&["disasmer", "project", "init", "--yes", "--json"],
&["disasmer", "inspect", "--json"],
&["disasmer", "build", "--json"],
&["disasmer", "bundle", "inspect", "--json"],
&["disasmer", "run", "--json"],
&["disasmer", "node", "attach", "--json"],
&["disasmer", "node", "enroll", "--json"],
&["disasmer", "process", "status", "--json"],
&["disasmer", "task", "list", "--json"],
&["disasmer", "logs", "--json"],
&["disasmer", "artifact", "list", "--json"],
&["disasmer", "artifact", "download", "artifact", "--json"],
&[
"disasmer", "artifact", "export", "artifact", "--to", "/tmp/out", "--json",
],
&["disasmer", "dap", "--plan", "--json"],
&["disasmer", "debug", "attach", "--json"],
&["disasmer", "quota", "status", "--json"],
&["disasmer", "admin", "status", "--json"],
] {
let _ = parse(args);
}
}
#[test]
fn human_report_is_text_not_json() {
let report = json!({
"command": "doctor",
"status": "ok",
"coordinator": "127.0.0.1:9443",
"next_actions": ["disasmer login --browser", "disasmer project init"],
});
let human = human_report(&report);
assert!(!human.trim_start().starts_with('{'));
assert!(human.contains("Disasmer doctor"));
assert!(human.contains("status: ok"));
assert!(human.contains("coordinator: 127.0.0.1:9443"));
assert!(human.contains("disasmer login --browser"));
}
#[test]
fn project_init_select_and_status_use_local_project_config() {
let temp = tempfile::tempdir().unwrap();
@ -2984,6 +3378,7 @@ mod tests {
BuildArgs {
project: Some(temp.path().to_path_buf()),
output: None,
json: false,
},
PathBuf::from("/unused"),
)