seaplane_cli/cli/cmds/formation/
delete.rs1use clap::{ArgMatches, Command};
2use seaplane::{api::ApiErrorKind, error::SeaplaneError};
3
4use crate::{
5 api::FormationsReq,
6 cli::{
7 cmds::{flight::SeaplaneFlightDelete, formation::SeaplaneFormationFetch},
8 errors,
9 validator::{validate_formation_name, validate_name_id},
10 CliCommand,
11 },
12 context::Ctx,
13 error::{CliErrorKind, Context, Result},
14 printer::Color,
15};
16
17#[derive(Copy, Clone, Debug)]
18pub struct SeaplaneFormationDelete;
19
20impl SeaplaneFormationDelete {
21 pub fn command() -> Command {
22 let validator = |s: &str| validate_name_id(validate_formation_name, s);
23 Command::new("delete")
25 .visible_aliases(["del", "remove", "rm"])
26 .about("Deletes local Formation Plans and/or remote Formation Instances")
27 .override_usage("
28 seaplane formation delete [OPTIONS] <NAME|ID>
29 seaplane formation delete [OPTIONS] <NAME|ID> --no-remote")
30 .arg(arg!(formation =["NAME|ID"] required)
31 .value_parser(validator)
32 .help("The name or ID of the Formation to remove, must be unambiguous"))
33 .arg(arg!(--recursive -('r'))
34 .help("Recursively delete all local definitions associated with this Formation"))
35 .arg(arg!(--force -('f'))
36 .help("Delete this Formation even if there are remote instances In Flight (active), which will effectively stop all remote instances of this Formation"))
37 .arg(arg!(--all -('a'))
38 .help("Delete all matching Formations even when the name or ID is ambiguous or a partial match"))
39 .arg(arg!(--local)
40 .overrides_with("no-local")
41 .help("Delete local Formation Definitions (this is set by the default, use --no-local to skip)"))
42 .arg(arg!(--("no-local"))
43 .overrides_with("local")
44 .help("DO NOT delete local Formation Definitions"))
45 .arg(arg!(--remote)
46 .overrides_with("no-remote")
47 .help("Delete remote Formation Instances (this is set by default, use --no-remote to skip)"))
48 .arg(arg!(--("no-remote"))
49 .overrides_with("remote")
50 .help("DO NOT delete remote Formation Instances (this is set by the default, use --remote to remove them)"))
51 .arg(arg!(--fetch|sync|synchronize - ('F')).help(
52 "Fetch remote Formation Instances and synchronize local Plan definitions prior to attempting to delete",
53 ))
54 }
55}
56
57impl CliCommand for SeaplaneFormationDelete {
58 fn run(&self, ctx: &mut Ctx) -> Result<()> {
59 if ctx.args.fetch {
60 let old_name = ctx.args.name_id.take();
61 ctx.internal_run = true;
62 SeaplaneFormationFetch.run(ctx)?;
63 ctx.internal_run = false;
64 ctx.args.name_id = old_name;
65 }
66
67 let formation_ctx = ctx.formation_ctx.get_or_init();
68
69 if !formation_ctx.local && !formation_ctx.remote {
70 cli_eprint!(@Red, "error: ");
71 cli_eprintln!("nothing to do");
72 cli_eprint!("(hint: either remove ");
73 cli_eprint!(@Yellow, "--no-local ");
74 cli_eprint!("or add ");
75 cli_eprint!(@Yellow, "--remote ");
76 cli_eprintln!("to the command)");
77 std::process::exit(1);
78 }
79
80 let indices = if ctx.args.all {
82 ctx.db
83 .formations
84 .formation_indices_of_left_matches(&formation_ctx.name_id)
85 } else {
86 ctx.db
87 .formations
88 .formation_indices_of_matches(&formation_ctx.name_id)
89 };
90
91 match indices.len() {
92 0 => errors::no_matching_item(formation_ctx.name_id.clone(), false, ctx.args.all)?,
93 1 => (),
94 _ => {
95 if !(ctx.args.all || ctx.args.force) {
96 errors::ambiguous_item(formation_ctx.name_id.clone(), true)?;
97 }
98 }
99 }
100
101 let mut deleted = indices.len();
102
103 if formation_ctx.remote {
108 let mut req = FormationsReq::new_delay_token(ctx)?;
109 for idx in &indices {
110 let formation = ctx.db.formations.get_formation(*idx).unwrap();
111 if let Some(name) = &formation.name {
112 req.set_name(name)?;
113 let cfg_uuids = match req.delete(ctx.args.force) {
114 Err(e) => {
115 if matches!(
116 e.kind(),
117 CliErrorKind::Seaplane(SeaplaneError::ApiResponse(ae))
118 if ae.kind == ApiErrorKind::NotFound)
119 {
120 continue;
121 }
122 return Err(e);
123 }
124 Ok(cfg_uuids) => cfg_uuids,
125 };
126 cli_print!("Deleted remote Formation Instance '");
127 cli_print!(@Green, "{}", name);
128 if cfg_uuids.is_empty() {
129 cli_println!("'");
130 } else {
131 cli_println!("' with Configuration UUIDs:");
132 for uuid in cfg_uuids.into_iter() {
133 cli_println!(@Green, "\t{uuid}");
134 }
135 }
136 } else {
137 return Err(CliErrorKind::NoMatchingItem(formation_ctx.name_id.clone())
138 .into_err()
139 .context("(hint: create the local Formation Plan with '")
140 .color_context(Color::Green, "seaplane formation plan")
141 .context("')\n")
142 .context("(hint: or try synchronizing local Plan definitions with remote instances by using '")
143 .color_context(Color::Green, "seaplane formation fetch-remote")
144 .context("')\n")
145 .context("(hint: You can also fetch remote instances by adding 'seaplane formation delete ")
146 .color_context(Color::Green, "--fetch")
147 .context("')\n"));
148 }
149 }
150 }
151 if formation_ctx.local && !ctx.args.stateless {
152 let mut cloned_ctx = ctx.clone();
154 for formation in ctx.db.formations.remove_formation_indices(&indices).iter() {
155 let ids = if ctx.args.force {
156 formation.local.iter().cloned().collect()
157 } else {
158 formation.local_only_configs()
159 };
160 for id in ids {
161 if let Some(cfg) = ctx.db.formations.remove_configuration(&id) {
162 if formation_ctx.recursive {
163 cloned_ctx.internal_run = true;
164 for flight in cfg.model.flights() {
165 let flight_name = flight.name();
166 if !ctx
167 .db
168 .formations
169 .configurations()
170 .filter(|cfg| cfg.id != id)
171 .any(|cfg| {
172 cfg.model.flights().iter().any(|f| f.name() == flight_name)
173 })
174 {
175 cloned_ctx.args.name_id = Some(flight_name.to_string());
176 SeaplaneFlightDelete.run(&mut cloned_ctx)?;
177 deleted += 1;
178 }
179 }
180 }
181 }
182 }
183 cli_println!("Deleted local Formation Plan {}", &formation.id.to_string());
184 }
185 }
186
187 ctx.persist_formations()?;
188
189 cli_println!(
191 "\nSuccessfully removed {} item{}",
192 deleted,
193 if deleted > 1 { "s" } else { "" }
194 );
195
196 Ok(())
197 }
198
199 fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
200 ctx.args.force = matches.get_flag("force");
201 ctx.args.all = matches.get_flag("all");
202 ctx.args.fetch = matches.get_flag("fetch");
203 let mut fctx = ctx.formation_ctx.get_mut_or_init();
204 fctx.name_id = matches.get_one::<String>("formation").unwrap().to_string();
205 fctx.remote = !matches.get_flag("no-remote");
206 fctx.local = !matches.get_flag("no-local");
207 fctx.recursive = matches.get_flag("recursive");
208
209 Ok(())
210 }
211}