Skip to content
Merged
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
2 changes: 1 addition & 1 deletion client-sdks/platform/openapi.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client-sdks/platform/rust/openapi.json

Large diffs are not rendered by default.

222 changes: 222 additions & 0 deletions crates/alien-cli/src/commands/deployments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ pub struct DeploymentsArgs {
pub cmd: DeploymentsCmd,
}

impl DeploymentsArgs {
pub fn wants_json_output(&self) -> bool {
matches!(
&self.cmd,
DeploymentsCmd::Create { format, .. } if format == "json"
) || matches!(
&self.cmd,
DeploymentsCmd::Ls { json: true, .. }
| DeploymentsCmd::Get { json: true, .. }
| DeploymentsCmd::Machines { json: true, .. }
| DeploymentsCmd::Retry { json: true, .. }
| DeploymentsCmd::Redeploy { json: true, .. }
| DeploymentsCmd::Pin { json: true, .. }
| DeploymentsCmd::SetChannel { json: true, .. }
)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

#[derive(Subcommand, Debug, Clone)]
pub enum DeploymentsCmd {
/// Create a new deployment
Expand Down Expand Up @@ -132,6 +150,15 @@ pub enum DeploymentsCmd {
#[arg(long)]
json: bool,
},
/// Show the machines connected to a deployment
Machines {
/// Deployment ID, or <deployment-group-name>/<deployment-name>
id: String,

/// Print the complete inventory as machine-readable JSON
#[arg(long)]
json: bool,
Comment thread
greptile-apps[bot] marked this conversation as resolved.
},
/// Delete a deployment
Delete {
/// Deployment ID, or <deployment-group-name>/<deployment-name>
Expand Down Expand Up @@ -248,6 +275,27 @@ pub async fn deployments_task(args: DeploymentsArgs, ctx: ExecutionMode) -> Resu
let manager = resolve_manager_client(&ctx, None, !json).await?;
get_deployment_task(&ctx, &manager, &id, json).await
}
DeploymentsCmd::Machines { id, json } => {
if !ctx.is_platform() {
return Err(AlienError::new(ErrorData::ValidationError {
field: "command".to_string(),
message: "Machine inventory requires platform mode.".to_string(),
}));
}
let workspace = ctx.resolve_workspace_with_bootstrap(!json).await?;
let client = ctx.sdk_client().await?;
let deployment = crate::platform_deployment_resolver::resolve(
&ctx, &client, &workspace, &id, None, !json,
)
.await?;
machines_inventory_task(
&client,
workspace.as_str(),
&String::from(deployment.id),
json,
)
.await
}
DeploymentsCmd::Delete { id, yes } => {
#[cfg(feature = "platform")]
if ctx.is_platform() {
Expand Down Expand Up @@ -418,6 +466,180 @@ pub async fn deployments_task(args: DeploymentsArgs, ctx: ExecutionMode) -> Resu
}
}

async fn machines_inventory_task(
client: &alien_platform_api::Client,
workspace: &str,
deployment_id: &str,
json: bool,
) -> Result<()> {
let mut response = client
.list_machines_inventory()
.id(deployment_id)
.workspace(workspace)
.send()
.await
.into_sdk_error()
.context(ErrorData::ApiRequestFailed {
message: format!("listing machines for deployment '{deployment_id}'"),
url: None,
})?
.into_inner();

if json {
return print_json(&response);
}

response.machines.sort_by(|left, right| {
machine_network_rank(right)
.cmp(&machine_network_rank(left))
.then_with(|| left.machine_id.cmp(&right.machine_id))
});
if response.machines.is_empty() {
println!("(no machines)");
return Ok(());
}

let mut table = make_table(&[
"Machine",
"Status",
"Heartbeat",
"Network",
"Peers",
"Machine agent",
]);
for machine in &response.machines {
let network = machine
.network_health
.map(|health| health.to_string())
.unwrap_or_else(|| "unknown".to_string());
let peers = if matches!(
machine.network_health,
Some(alien_platform_api::types::MachinesInventoryItemNetworkHealth::Healthy)
| Some(alien_platform_api::types::MachinesInventoryItemNetworkHealth::Degraded)
) {
machine
.wireguard_mesh
.as_ref()
.and_then(|observation| observation.0.as_ref())
.map(|observation| {
format!(
"{}/{}",
observation.reachable_peer_count, observation.expected_peer_count
)
})
.unwrap_or_else(|| "—".to_string())
} else {
"—".to_string()
};
let version = machine
.horizond_version
.as_ref()
.cloned()
.unwrap_or_else(|| "unknown".to_string());
table.add_row(vec![
machine.machine_id.clone().into(),
status_cell(&machine.status),
machine.last_heartbeat.clone().into(),
network.into(),
peers.into(),
version.into(),
]);
}
print_table(table);

for machine in response
.machines
.iter()
.filter(|machine| machine_network_rank(machine) > 0)
{
if let Some(observation) = machine
.wireguard_mesh
.as_ref()
.and_then(|observation| observation.0.as_ref())
{
if matches!(
machine.network_health,
Some(alien_platform_api::types::MachinesInventoryItemNetworkHealth::Degraded)
) && !observation.missing_peer_machine_ids.is_empty()
{
println!(
"{} missing peers: {}",
machine.machine_id,
observation.missing_peer_machine_ids.join(", ")
);
}
}
}

Ok(())
}

fn machine_network_rank(machine: &alien_platform_api::types::MachinesInventoryItem) -> u8 {
network_health_rank(machine.network_health)
}

fn network_health_rank(
health: Option<alien_platform_api::types::MachinesInventoryItemNetworkHealth>,
) -> u8 {
match health {
Some(alien_platform_api::types::MachinesInventoryItemNetworkHealth::Degraded) => 2,
Some(alien_platform_api::types::MachinesInventoryItemNetworkHealth::Unknown)
| Some(alien_platform_api::types::MachinesInventoryItemNetworkHealth::Healthy)
| None => 0,
}
}

#[cfg(test)]
mod machines_inventory_tests {
use super::*;
use alien_platform_api::types::MachinesInventoryItemNetworkHealth;

#[test]
fn machine_inventory_sorts_network_divergence_first() {
assert_eq!(
network_health_rank(Some(MachinesInventoryItemNetworkHealth::Degraded)),
2
);
assert_eq!(network_health_rank(None), 0);
assert_eq!(
network_health_rank(Some(MachinesInventoryItemNetworkHealth::Healthy)),
0
);
}

#[test]
fn machine_inventory_json_mode_routes_errors_as_json() {
let args = DeploymentsArgs {
cmd: DeploymentsCmd::Machines {
id: "deployment".to_string(),
json: true,
},
};
assert!(args.wants_json_output());
}

#[test]
fn create_json_format_routes_errors_as_json() {
let args = DeploymentsArgs::try_parse_from([
"deployments",
"create",
"--name",
"example",
"--project",
"project",
"--deployment-group",
"group",
"--platform",
"aws",
"--format",
"json",
])
.expect("create arguments should parse");

assert!(args.wants_json_output());
}
}

// ---------------------------------------------------------------------------
// Manager client resolution
// ---------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions crates/alien-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,12 @@ impl Cli {
Some(Commands::Logs(args)) => args.json,
Some(Commands::Whoami(args)) => args.json,
Some(Commands::Debug(args)) => args.wants_json_output(),
Some(Commands::Deployments(args)) => args.wants_json_output(),
Some(Commands::Dev(dev)) => match &dev.subcommand {
Some(DevSubcommand::Release(args)) => args.json,
Some(DevSubcommand::Whoami(args)) => args.json,
Some(DevSubcommand::Debug(args)) => args.wants_json_output(),
Some(DevSubcommand::Deployments(args)) => args.wants_json_output(),
_ => false,
},
#[cfg(feature = "platform")]
Expand Down
Loading
Loading