Skip to main content

seaplane_cli/cli/cmds/formation/
delete.rs

1use 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        // TODO: add --recursive to handle configurations too
24        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        // Get the indices of any formations that match the given name/ID
81        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        // Remove the Formations
104        //
105        // First try to delete the remote formation if required, because we don't want to delete
106        // the local one too if this fails
107        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            // No need to potentially clone over and over
153            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        // TODO: recalculate dichotomy of local v. remote numbers (i.e. --no-local, etc.)
190        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}