use crate::config::{self, ConfigEntity};
use crate::error::{Error, Result};
use crate::output::{CreateOutput, MergeOutput, RemoveResult};
use crate::project;
use crate::project::ProjectComponentOverrides;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub mod check;
pub mod exec;
pub mod status;
pub use check::{collect_check, FleetCheckSummary, FleetComponentCheck, FleetProjectCheck};
pub use exec::{collect_exec, FleetExecProjectResult, FleetExecSummary};
pub use status::{
collect_status, FleetComponentDrift, FleetComponentStatus, FleetComponentSummary,
FleetProjectStatus, FleetProjectSummary, FleetServerSummary, FleetStatusResult,
FleetStatusSummary, FleetWarning,
};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Fleet {
#[serde(skip_deserializing, default)]
pub id: String,
#[serde(default)]
pub project_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub component_overrides: HashMap<String, ProjectComponentOverrides>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority_labels: Option<Vec<String>>,
}
impl Fleet {
pub fn new(id: String, project_ids: Vec<String>) -> Self {
Self {
id,
project_ids,
description: None,
component_overrides: HashMap::new(),
priority_labels: None,
}
}
}
impl ConfigEntity for Fleet {
const ENTITY_TYPE: &'static str = "fleet";
const DIR_NAME: &'static str = "fleets";
fn id(&self) -> &str {
&self.id
}
fn set_id(&mut self, id: String) {
self.id = id;
}
fn not_found_error(id: String, suggestions: Vec<String>) -> Error {
Error::fleet_not_found(id, suggestions)
}
}
entity_crud!(Fleet; list_ids, merge);
pub fn add_project(fleet_id: &str, project_id: &str) -> Result<Fleet> {
let mut fleet = load(fleet_id)?;
if !project::exists(project_id) {
let suggestions = config::find_similar_ids::<crate::project::Project>(project_id);
return Err(Error::project_not_found(project_id, suggestions));
}
if !fleet.project_ids.contains(&project_id.to_string()) {
fleet.project_ids.push(project_id.to_string());
save(&fleet)?;
}
Ok(fleet)
}
pub fn remove_project(fleet_id: &str, project_id: &str) -> Result<Fleet> {
let mut fleet = load(fleet_id)?;
fleet.project_ids.retain(|id| id != project_id);
save(&fleet)?;
Ok(fleet)
}
pub fn get_projects(fleet_id: &str) -> Result<Vec<crate::project::Project>> {
let fleet = load(fleet_id)?;
let mut projects = Vec::new();
for project_id in &fleet.project_ids {
if let Ok(project) = project::load(project_id) {
projects.push(project);
}
}
Ok(projects)
}
pub fn component_usage(fleet_id: &str) -> Result<std::collections::HashMap<String, Vec<String>>> {
let fleet = load(fleet_id)?;
let mut usage: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for project_id in &fleet.project_ids {
if let Ok(project) = project::load(project_id) {
for component_id in project::project_component_ids(&project) {
usage
.entry(component_id)
.or_default()
.push(project_id.clone());
}
}
}
Ok(usage)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fleet_new_has_empty_component_overrides() {
let fleet = Fleet::new("test-fleet".to_string(), vec!["project-a".to_string()]);
assert!(fleet.component_overrides.is_empty());
}
#[test]
fn fleet_component_overrides_serialization_roundtrip() {
let mut fleet = Fleet::new("test-fleet".to_string(), vec![]);
fleet.component_overrides.insert(
"my-plugin".to_string(),
ProjectComponentOverrides {
remote_path: Some("wp-content/plugins/my-plugin".to_string()),
remote_owner: Some("www-data:www-data".to_string()),
..Default::default()
},
);
let json = serde_json::to_string(&fleet).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let overrides = parsed.get("component_overrides").unwrap();
let plugin = overrides.get("my-plugin").unwrap();
assert_eq!(
plugin.get("remote_path").unwrap().as_str(),
Some("wp-content/plugins/my-plugin")
);
assert_eq!(
plugin.get("remote_owner").unwrap().as_str(),
Some("www-data:www-data")
);
}
#[test]
fn fleet_empty_component_overrides_not_serialized() {
let fleet = Fleet::new("test-fleet".to_string(), vec![]);
let json = serde_json::to_string(&fleet).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("component_overrides").is_none());
}
#[test]
fn fleet_priority_labels_serialization_roundtrip() {
let mut fleet = Fleet::new("test-fleet".to_string(), vec![]);
fleet.priority_labels = Some(vec!["urgent".to_string(), "release-blocker".to_string()]);
let json = serde_json::to_string(&fleet).unwrap();
let parsed: Fleet = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.priority_labels,
Some(vec!["urgent".to_string(), "release-blocker".to_string()])
);
}
}