use clap::{Args, Subcommand};
use homeboy::log_status;
use serde::Serialize;
use homeboy::component;
use homeboy::deploy::{self, DeployConfig};
use homeboy::fleet::{self, Fleet};
use homeboy::project::{self, Project};
use homeboy::version;
use homeboy::EntityCrudOutput;
use super::{CmdResult, DynamicSetArgs};
#[derive(Args)]
pub struct FleetArgs {
#[command(subcommand)]
command: FleetCommand,
}
#[derive(Subcommand)]
enum FleetCommand {
Create {
id: String,
#[arg(long, short = 'p', value_delimiter = ',')]
projects: Option<Vec<String>>,
#[arg(long, short = 'd')]
description: Option<String>,
},
Show {
id: String,
},
#[command(visible_aliases = ["edit", "merge"])]
Set {
#[command(flatten)]
args: DynamicSetArgs,
},
Delete {
id: String,
},
List,
Add {
id: String,
#[arg(long, short = 'p')]
project: String,
},
Remove {
id: String,
#[arg(long, short = 'p')]
project: String,
},
Projects {
id: String,
},
Components {
id: String,
},
Status {
id: String,
},
Check {
id: String,
#[arg(long)]
outdated: bool,
},
Sync {
id: String,
#[arg(long, short = 'c', value_delimiter = ',')]
category: Option<Vec<String>>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
leader: Option<String>,
},
}
#[derive(Debug, Default, Serialize)]
pub struct FleetExtra {
#[serde(skip_serializing_if = "Option::is_none")]
pub projects: Option<Vec<Project>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<std::collections::HashMap<String, Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<Vec<FleetProjectStatus>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub check: Option<Vec<FleetProjectCheck>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<FleetCheckSummary>,
}
pub type FleetOutput = EntityCrudOutput<Fleet, FleetExtra>;
#[derive(Debug, Default, Serialize)]
pub struct FleetProjectCheck {
pub project_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_id: Option<String>,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub components: Vec<FleetComponentCheck>,
}
#[derive(Debug, Default, Serialize)]
pub struct FleetComponentCheck {
pub component_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub local_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_version: Option<String>,
pub status: String,
}
#[derive(Debug, Default, Serialize)]
pub struct FleetCheckSummary {
pub total_projects: u32,
pub projects_checked: u32,
pub projects_failed: u32,
pub components_up_to_date: u32,
pub components_needs_update: u32,
pub components_unknown: u32,
}
#[derive(Debug, Default, Serialize)]
pub struct FleetProjectStatus {
pub project_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_id: Option<String>,
pub components: Vec<FleetComponentStatus>,
}
#[derive(Debug, Default, Serialize)]
pub struct FleetComponentStatus {
pub component_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
pub fn run(args: FleetArgs, _global: &super::GlobalArgs) -> CmdResult<FleetOutput> {
match args.command {
FleetCommand::Create {
id,
projects,
description,
} => create(&id, projects.unwrap_or_default(), description),
FleetCommand::Show { id } => show(&id),
FleetCommand::Set { args } => set(args),
FleetCommand::Delete { id } => delete(&id),
FleetCommand::List => list(),
FleetCommand::Add { id, project } => add(&id, &project),
FleetCommand::Remove { id, project } => remove(&id, &project),
FleetCommand::Projects { id } => projects(&id),
FleetCommand::Components { id } => components(&id),
FleetCommand::Status { id } => status(&id),
FleetCommand::Check { id, outdated } => check(&id, outdated),
FleetCommand::Sync {
id,
category,
dry_run,
leader,
} => sync(&id, category, dry_run, leader),
}
}
fn create(
id: &str,
project_ids: Vec<String>,
description: Option<String>,
) -> CmdResult<FleetOutput> {
for pid in &project_ids {
if !homeboy::project::exists(pid) {
return Err(homeboy::Error::project_not_found(pid, vec![]));
}
}
let mut new_fleet = Fleet::new(id.to_string(), project_ids);
new_fleet.description = description;
let json_spec = homeboy::config::to_json_string(&new_fleet)?;
match fleet::create(&json_spec, false)? {
homeboy::CreateOutput::Single(result) => Ok((
FleetOutput {
command: "fleet.create".to_string(),
id: Some(result.id),
entity: Some(result.entity),
..Default::default()
},
0,
)),
homeboy::CreateOutput::Bulk(_) => Err(homeboy::Error::internal_unexpected(
"Unexpected bulk result for single fleet".to_string(),
)),
}
}
fn show(id: &str) -> CmdResult<FleetOutput> {
let fl = fleet::load(id)?;
Ok((
FleetOutput {
command: "fleet.show".to_string(),
id: Some(id.to_string()),
entity: Some(fl),
..Default::default()
},
0,
))
}
fn set(args: DynamicSetArgs) -> CmdResult<FleetOutput> {
let merged = super::merge_dynamic_args(&args)?.ok_or_else(|| {
homeboy::Error::validation_invalid_argument(
"spec",
"Provide JSON spec, --json flag, --base64 flag, or --key value flags",
None,
None,
)
})?;
let (json_string, replace_fields) = super::finalize_set_spec(&merged, &args.replace)?;
match fleet::merge(args.id.as_deref(), &json_string, &replace_fields)? {
homeboy::MergeOutput::Single(result) => {
let fl = fleet::load(&result.id)?;
Ok((
FleetOutput {
command: "fleet.set".to_string(),
id: Some(result.id),
entity: Some(fl),
updated_fields: result.updated_fields,
..Default::default()
},
0,
))
}
homeboy::MergeOutput::Bulk(_) => Err(homeboy::Error::internal_unexpected(
"Unexpected bulk result for single fleet".to_string(),
)),
}
}
fn delete(id: &str) -> CmdResult<FleetOutput> {
fleet::delete(id)?;
Ok((
FleetOutput {
command: "fleet.delete".to_string(),
id: Some(id.to_string()),
deleted: vec![id.to_string()],
..Default::default()
},
0,
))
}
fn list() -> CmdResult<FleetOutput> {
let fleets = fleet::list()?;
Ok((
FleetOutput {
command: "fleet.list".to_string(),
entities: fleets,
..Default::default()
},
0,
))
}
fn add(fleet_id: &str, project_id: &str) -> CmdResult<FleetOutput> {
let fl = fleet::add_project(fleet_id, project_id)?;
Ok((
FleetOutput {
command: "fleet.add".to_string(),
id: Some(fleet_id.to_string()),
entity: Some(fl),
updated_fields: vec!["project_ids".to_string()],
..Default::default()
},
0,
))
}
fn remove(fleet_id: &str, project_id: &str) -> CmdResult<FleetOutput> {
let fl = fleet::remove_project(fleet_id, project_id)?;
Ok((
FleetOutput {
command: "fleet.remove".to_string(),
id: Some(fleet_id.to_string()),
entity: Some(fl),
updated_fields: vec!["project_ids".to_string()],
..Default::default()
},
0,
))
}
fn projects(id: &str) -> CmdResult<FleetOutput> {
let projects = fleet::get_projects(id)?;
Ok((
FleetOutput {
command: "fleet.projects".to_string(),
id: Some(id.to_string()),
extra: FleetExtra {
projects: Some(projects),
..Default::default()
},
..Default::default()
},
0,
))
}
fn components(id: &str) -> CmdResult<FleetOutput> {
let components = fleet::component_usage(id)?;
Ok((
FleetOutput {
command: "fleet.components".to_string(),
id: Some(id.to_string()),
extra: FleetExtra {
components: Some(components),
..Default::default()
},
..Default::default()
},
0,
))
}
fn status(id: &str) -> CmdResult<FleetOutput> {
let fl = fleet::load(id)?;
let mut project_statuses = Vec::new();
for project_id in &fl.project_ids {
let proj = match project::load(project_id) {
Ok(p) => p,
Err(_) => continue,
};
let mut component_statuses = Vec::new();
for component_id in &proj.component_ids {
let comp_version = match component::load(component_id) {
Ok(comp) => version::get_component_version(&comp),
Err(_) => None,
};
component_statuses.push(FleetComponentStatus {
component_id: component_id.clone(),
version: comp_version,
});
}
project_statuses.push(FleetProjectStatus {
project_id: project_id.clone(),
server_id: proj.server_id.clone(),
components: component_statuses,
});
}
Ok((
FleetOutput {
command: "fleet.status".to_string(),
id: Some(id.to_string()),
extra: FleetExtra {
status: Some(project_statuses),
..Default::default()
},
..Default::default()
},
0,
))
}
fn check(id: &str, only_outdated: bool) -> CmdResult<FleetOutput> {
let fl = fleet::load(id)?;
let mut project_checks = Vec::new();
let mut summary = FleetCheckSummary {
total_projects: fl.project_ids.len() as u32,
..Default::default()
};
for project_id in &fl.project_ids {
log_status!("fleet", "Checking project '{}'...", project_id);
let config = DeployConfig {
component_ids: vec![],
all: true,
outdated: false,
dry_run: false,
check: true,
force: false,
skip_build: true,
keep_deps: false, };
match deploy::run(project_id, &config) {
Ok(result) => {
summary.projects_checked += 1;
let proj = project::load(project_id).ok();
let mut component_checks = Vec::new();
for comp_result in &result.results {
let status_str = match &comp_result.component_status {
Some(deploy::ComponentStatus::UpToDate) => "up_to_date",
Some(deploy::ComponentStatus::NeedsUpdate) => "needs_update",
Some(deploy::ComponentStatus::BehindRemote) => "behind_remote",
Some(deploy::ComponentStatus::Unknown) | None => "unknown",
};
match status_str {
"up_to_date" => summary.components_up_to_date += 1,
"needs_update" | "behind_remote" => summary.components_needs_update += 1,
_ => summary.components_unknown += 1,
}
if only_outdated && status_str == "up_to_date" {
continue;
}
component_checks.push(FleetComponentCheck {
component_id: comp_result.id.clone(),
local_version: comp_result.local_version.clone(),
remote_version: comp_result.remote_version.clone(),
status: status_str.to_string(),
});
}
if only_outdated && component_checks.is_empty() {
continue;
}
project_checks.push(FleetProjectCheck {
project_id: project_id.clone(),
server_id: proj.and_then(|p| p.server_id),
status: "checked".to_string(),
error: None,
components: component_checks,
});
}
Err(e) => {
summary.projects_failed += 1;
if !only_outdated {
project_checks.push(FleetProjectCheck {
project_id: project_id.clone(),
server_id: None,
status: "failed".to_string(),
error: Some(e.to_string()),
components: vec![],
});
}
}
}
}
let exit_code = if summary.projects_failed > 0 { 1 } else { 0 };
Ok((
FleetOutput {
command: "fleet.check".to_string(),
id: Some(id.to_string()),
extra: FleetExtra {
check: Some(project_checks),
summary: Some(summary),
..Default::default()
},
..Default::default()
},
exit_code,
))
}
fn sync(
_id: &str,
_categories: Option<Vec<String>>,
_dry_run: bool,
_leader_override: Option<String>,
) -> CmdResult<FleetOutput> {
Err(homeboy::Error::validation_invalid_argument(
"fleet sync",
"fleet sync has been deprecated. Use 'homeboy deploy' to sync files across servers. \
Register your agent workspace as a component and deploy it like any other component.",
None,
None,
)
.with_hint("homeboy deploy <component> --fleet <fleet>".to_string())
.with_hint("See: https://github.com/Extra-Chill/homeboy/issues/101".to_string()))
}