seaplane_cli/cli/cmds/formation/
plan.rs

1use clap::{ArgMatches, Command};
2use const_format::concatcp;
3
4#[cfg(not(any(feature = "ui_tests", feature = "semantic_ui_tests")))]
5use crate::cli::cmds::flight::SeaplaneFlightPlan;
6use crate::{
7    cli::{
8        cmds::formation::{common, SeaplaneFormationFetch, SeaplaneFormationLaunch},
9        specs::{FLIGHT_SPEC, REGION_SPEC},
10        CliCommand,
11    },
12    context::{Ctx, FlightCtx},
13    error::{CliErrorKind, Context, Result},
14    ops::formation::{Formation, FormationConfiguration},
15    printer::Color,
16};
17
18static LONG_ABOUT: &str =
19    "Make a new local Formation Plan (and optionally launch an instance of it)
20
21Include local Flight Plans by using `--include-flight-plan`. Multiple Flights may be included in a
22Formation Plan using a SEMICOLON separated list, or using the argument multiple times.
23
24You can also create a new Flight Plan using the INLINE-SPEC option of `--include-flight-plan`.
25
26Flight Plans created using INLINE-SPEC are automatically included in the Formation Plan.";
27
28/// A newtype wrapper to enforce where the ArgMatches came from which reduces errors in checking if
29/// values of arguments were used or not. i.e. `seaplane formation plan` may not have the same
30/// arguments as `seaplane account token` even though both produce an `ArgMatches`.
31#[allow(missing_debug_implementations)]
32pub struct SeaplaneFormationPlanArgMatches<'a>(pub &'a ArgMatches);
33
34#[derive(Copy, Clone, Debug)]
35pub struct SeaplaneFormationPlan;
36
37impl SeaplaneFormationPlan {
38    pub fn command() -> Command {
39        Command::new("plan")
40            .after_help(concatcp!(FLIGHT_SPEC, "\n\n", REGION_SPEC))
41            .visible_aliases(["create", "add"])
42            .about("Create a Seaplane Formation")
43            .long_about(LONG_ABOUT)
44            .args(common::args())
45            .arg(arg!(--force).help("Override any existing Formation with the same NAME"))
46            .arg(arg!(--fetch|sync|synchronize - ('F')).help("Fetch remote instances prior to creating this plan to check for conflicts (by default only local references are considered)"))
47    }
48}
49
50impl CliCommand for SeaplaneFormationPlan {
51    fn run(&self, ctx: &mut Ctx) -> Result<()> {
52        if ctx.args.fetch {
53            let old_name = ctx.args.name_id.take();
54            ctx.internal_run = true;
55            SeaplaneFormationFetch.run(ctx)?;
56            ctx.internal_run = false;
57            ctx.args.name_id = old_name;
58        }
59
60        let formation_ctx = ctx.formation_ctx.get_or_init();
61
62        // Check for duplicates and suggest `seaplane formation edit`
63        let name = &formation_ctx.name_id;
64        if ctx.db.formations.contains_name(name) {
65            if !ctx.args.force {
66                let mut err = CliErrorKind::DuplicateName(name.to_owned())
67                    .into_err()
68                    .context("(hint: try '")
69                    .color_context(Color::Green, format!("seaplane formation edit {}", &name))
70                    .context("' instead)\n");
71                if ctx.db.needs_persist {
72                    err = err.context("\nRolling back created Flight Plans!\n");
73                }
74                return Err(err);
75            }
76
77            // We have duplicates, but the user passed --force. So first we remove the existing
78            // formations and "re-add" them
79
80            // TODO: We should check if these ones we remove are referenced remote or not
81            // TODO: if more than one formation has the exact same name, we remove them all; that's
82            // *probably* what we want? But more thought should go into this...
83            ctx.db.formations.remove_name(name);
84        }
85
86        if ctx.db.needs_persist {
87            // Any flights we created in update_ctx can now be persisted. We didn't want to persist
88            // them before as we could have still hit an error such as Duplicate Formation Names
89            ctx.persist_flights()?;
90            ctx.db.needs_persist = false;
91        }
92
93        // Add the new formation
94        let mut new_formation = Formation::new(&formation_ctx.name_id);
95
96        let cfg = formation_ctx.configuration_model(ctx)?;
97        let formation_cfg = FormationConfiguration::new(cfg);
98        new_formation.local.insert(formation_cfg.id);
99        ctx.db.formations.configurations.push(formation_cfg);
100
101        let id = new_formation.id.to_string();
102        ctx.db.formations.formations.push(new_formation);
103
104        ctx.persist_formations()?;
105
106        cli_print!("Successfully created local Formation Plan '");
107        cli_print!(@Green, "{}", &formation_ctx.name_id);
108        cli_print!("' with ID '");
109        cli_print!(@Green, "{}", &id[..8]);
110        cli_println!("'");
111
112        // Equivalent of doing 'seaplane formation launch NAME --exact'
113        if formation_ctx.launch || formation_ctx.grounded {
114            // Set the name of the formation for launch
115            ctx.args.name_id = Some(formation_ctx.name_id.clone());
116            // We only want to match this exact formation
117            ctx.args.exact = true;
118            // If `--fetch` was passed, we already did it, no need to do it again
119            ctx.args.fetch = false;
120            // release the MutexGuard
121            SeaplaneFormationLaunch.run(ctx)?;
122        }
123
124        Ok(())
125    }
126
127    fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
128        ctx.args.fetch = matches.get_flag("fetch");
129        ctx.args.force = matches.get_flag("force");
130
131        // Create any flights required
132        let mut flights: Vec<_> = matches
133            .get_many::<String>("include-flight-plan")
134            .unwrap_or_default()
135            .collect();
136
137        // Flights declared with i.e. name=FOO,image=nginx:latest
138        let inline_flights = vec_remove_if!(flights, |f: &str| f.contains('='));
139        for flight in inline_flights {
140            let mut cloned_ctx = ctx.clone();
141            // We set stateless because we don't want the created flights to be persisted until
142            // we're ready (i.e. we're sure this formation will be created)
143            cloned_ctx.args.stateless = true;
144            cloned_ctx.internal_run = true;
145            cloned_ctx
146                .flight_ctx
147                .init(FlightCtx::from_inline_flight(flight, &ctx.registry)?);
148
149            #[cfg(not(any(feature = "ui_tests", feature = "semantic_ui_tests")))]
150            {
151                let flight_plan: Box<dyn CliCommand> = Box::new(SeaplaneFlightPlan);
152                flight_plan.run(&mut cloned_ctx)?;
153            }
154
155            let name = cloned_ctx.flight_ctx.get_or_init().name_id.clone();
156            // copy the newly created flight out of the cloned context into the "real" one
157            #[cfg(not(any(feature = "ui_tests", feature = "semantic_ui_tests")))]
158            ctx.db
159                .flights
160                .add_flight(cloned_ctx.db.flights.remove_flight(&name, true).unwrap());
161
162            // Store the newly created Flight name as if it was passed by name via
163            // `--include-flight-plan FOO`
164            ctx.formation_ctx
165                .get_mut_or_init()
166                .cfg_ctx
167                .flights
168                .push(name);
169
170            ctx.db.needs_persist = true;
171        }
172
173        // Flights using @path or @-
174        #[cfg(not(any(feature = "ui_tests", feature = "semantic_ui_tests")))]
175        for name in ctx
176            .db
177            .flights
178            .add_from_at_strs(vec_remove_if!(flights, |f: &str| f.starts_with('@')))?
179        {
180            ctx.formation_ctx
181                .get_mut_or_init()
182                .cfg_ctx
183                .flights
184                .push(name);
185            ctx.db.needs_persist = true;
186        }
187
188        // Any flights we created will be persisted during `run` as we could still hit an error
189        // such as Duplicate Formation Names and would need to roll them back
190
191        ctx.formation_ctx
192            .get_mut_or_init()
193            .update_from_formation_plan(
194                &SeaplaneFormationPlanArgMatches(matches),
195                &ctx.db.flights,
196            )?;
197
198        Ok(())
199    }
200}