seaplane_cli/cli/cmds/formation/
launch.rs1use clap::{ArgMatches, Command};
2use seaplane::{
3 api::{
4 compute::v1::{ActiveConfiguration, ActiveConfigurations},
5 ApiErrorKind,
6 },
7 error::SeaplaneError,
8};
9
10use crate::{
11 api::FormationsReq,
12 cli::{
13 cmds::formation::SeaplaneFormationFetch,
14 errors,
15 validator::{validate_formation_name, validate_name_id},
16 CliCommand,
17 },
18 context::Ctx,
19 error::{CliErrorKind, Context, Result},
20 printer::{Color, Pb},
21};
22
23static LONG_ABOUT: &str = "Start a local Formation Plan creating a remote Formation Instance
24
25In many cases, or at least initially a local Formation Plan may only have a single
26Formation Configuration. In these cases this command will set that one configuration to active
27creating a remote Formation Instance with a single configuration.
28
29Things become slightly more complex when there are multiple Formation Configurations. Let's
30look at each possibility in turn.
31
32\"Local Only\" Configs Exist:
33
34A \"Local Only\" Config is a configuration that exists in the local database, but has not (yet)
35been uploaded to the Seaplane Cloud.
36
37In these cases the configurations will be sent to the Seaplane Cloud, and set to active. If the
38Seaplane Cloud already has configurations for the given Formation (either active or inactive),
39these new configurations will be appended, and traffic will be balanced between any *all*
40configurations.
41
42\"Remote Active\" Configs Exist:
43
44A \"Remote Active\" Config is a configuration that the Seaplane Cloud is aware of, and actively
45sending traffic to.
46
47These configurations will remain active and traffic will be balanced between any *all*
48configurations.
49
50\"Remote Inactive\" Configs Exist:
51
52A \"Remote Inactive\" Config is a configuration that the Seaplane Cloud is aware of, and but not
53sending traffic to.
54
55These configurations will be made active. If the Seaplane Cloud already has active
56configurations for the given Formation, these newly activated configurations will be appended,
57and traffic will be balanced between any *all* configurations. ";
58
59#[derive(Copy, Clone, Debug)]
60pub struct SeaplaneFormationLaunch;
61
62impl SeaplaneFormationLaunch {
63 pub fn command() -> Command {
64 let validator = |s: &str| validate_name_id(validate_formation_name, s);
65
66 Command::new("launch")
68 .visible_alias("start")
69 .about("Start a local Formation Plan creating a remote Formation Instance")
70 .long_about(LONG_ABOUT)
71 .arg(
72 arg!(formation =["NAME|ID"] required)
73 .value_parser(validator)
74 .help("The name or ID of the Formation Plan to launch and create an Instance of"),
75 )
76 .arg(
77 arg!(--all - ('a'))
78 .help("Launch all matching local Formation Plans even when the name or ID is ambiguous"),
79 )
80 .arg(arg!(--fetch|sync|synchronize - ('F')).help(
81 "Fetch remote Formation Instances and synchronize local Plan definitions prior to attempting to launch",
82 ))
83 .arg(
84 arg!(--grounded).help(
85 "Upload the configuration(s) defined in this local Formation Plan to Seaplane but *DO NOT* set them to active",
86 ),
87 )
88 }
89}
90
91impl CliCommand for SeaplaneFormationLaunch {
92 fn run(&self, ctx: &mut Ctx) -> Result<()> {
93 let name = ctx.args.name_id.as_ref().unwrap().clone();
94 if ctx.args.fetch {
95 let old_name = ctx.args.name_id.take();
96 let old_ir = ctx.internal_run;
97 ctx.internal_run = true;
98 SeaplaneFormationFetch.run(ctx)?;
99 ctx.internal_run = old_ir;
100 ctx.args.name_id = old_name;
101 }
102 let indices = if ctx.args.all {
104 ctx.db.formations.formation_indices_of_left_matches(&name)
105 } else {
106 ctx.db.formations.formation_indices_of_matches(&name)
107 };
108
109 match indices.len() {
110 0 => errors::no_matching_item(name, false, ctx.args.all)?,
111 1 => (),
112 _ => {
113 if !ctx.args.all {
115 errors::ambiguous_item(name, true)?;
116 }
117 }
118 }
119
120 let pb = Pb::new(ctx);
121 let grounded = ctx.formation_ctx.get_or_init().grounded;
122 let mut req = FormationsReq::new_delay_token(ctx)?;
123 for idx in indices {
124 let formation = ctx.db.formations.get_formation(idx).unwrap();
126 let formation_name = formation.name.as_ref().unwrap().clone();
127 req.set_name(&formation_name)?;
128
129 let cfgs_ids = if grounded {
131 formation.local_only_configs()
132 } else {
133 formation.local_or_grounded_configs()
134 };
135
136 if cfgs_ids.is_empty() {
137 if ctx.args.fetch && grounded {
138 cli_println!("Formation Plan '{formation_name}' is already uploaded and in status Grounded");
139 return Ok(());
140 } else {
141 return Err(CliErrorKind::OneOff(
142 "Cannot find local Formation Plan '{formation_name}'".into(),
143 )
144 .into_err())
145 .context(
146 "(hint: you can try adding '--fetch' to synchronize remote instances)\n",
147 );
148 }
149 }
150
151 let mut created_new = false;
153 let mut has_public_endpoints = false;
154 let mut cfg_uuids = Vec::new();
155
156 'inner: for id in &cfgs_ids {
158 if let Some(cfg) = ctx.db.formations.get_configuration(id) {
159 has_public_endpoints = cfg.model.public_endpoints().count() > 0;
160 if let Some(flight) = cfg.model.public_endpoints().find_map(|(_, dst)| {
161 if !ctx.db.formations.has_flight(&dst.flight_name) {
162 Some(dst.flight_name.clone())
163 } else {
164 None
165 }
166 }) {
167 return Err(CliErrorKind::EndpointInvalidFlight(flight).into_err()
168 .context("perhaps the Flight Plan exists, but only in a remote Formation Instance?\n")
169 .context("(hint: use '")
170 .color_context(Color::Yellow, "--fetch")
171 .context("' to synchronize local Plan definitions with remote Instances)\n")
172 .context("(hint: alternatively, you create the Flight Plan with '")
173 .color_context(Color::Green, "seaplane flight plan")
174 .context("')\n"));
175 }
176
177 pb.set_message("Searching for existing Formations...");
180 if let Err(e) = req.add_configuration(&cfg.model, false) {
181 match e.kind() {
182 CliErrorKind::Seaplane(SeaplaneError::ApiResponse(ae))
183 if ae.kind == ApiErrorKind::NotFound =>
184 {
185 pb.set_message("Creating new Formation Instance...");
187 let cfg_uuid = req.create(&cfg.model, !grounded)?;
188 cfg_uuids.extend(cfg_uuid);
189 ctx.db.formations.add_in_air_by_name(&formation_name, *id);
190 created_new = true;
191 break 'inner;
192 }
193 _ => return Err(e),
194 }
195 } else {
196 pb.set_message("Found existing Formation Instance...");
197 ctx.db.formations.add_grounded_by_name(&formation_name, *id);
198 }
199 } else {
200 unreachable!()
204 }
205 }
206
207 if !grounded && !created_new {
210 pb.set_message("Synchronizing Formation Configurations...");
212 cfg_uuids.extend(
213 req.list_configuration_ids()
214 .context("Context: failed to retrieve Formation Configuration IDs\n")?,
215 );
216 let mut active_configs = ActiveConfigurations::new();
217 for uuid in &cfg_uuids {
218 #[cfg_attr(not(feature = "unstable"), allow(unused_mut))]
219 let mut cfg = ActiveConfiguration::builder().uuid(*uuid);
220 #[cfg(feature = "unstable")]
221 {
222 cfg = cfg.traffic_weight(1.0);
223 }
224 active_configs.add_configuration_mut(cfg.build()?);
225 }
226 pb.set_message("Adding Formation Configurations to remote Instance...");
227 req.set_active_configurations(&active_configs, false)
228 .context("Context: failed to start Formation\n")?;
229 for id in cfgs_ids {
230 ctx.db.formations.add_in_air_by_name(&formation_name, id);
231 }
232 }
233 pb.set_message("Getting Formation URL...");
234 let domain = req.get_metadata()?.url;
235
236 pb.finish_and_clear();
237 cli_print!("Successfully Launched remote Formation Instance '");
238 cli_print!(@Green, "{}", &ctx.args.name_id.as_ref().unwrap());
239 if cfg_uuids.is_empty() {
240 cli_println!("'");
241 } else {
242 cli_println!("' with Configuration UUIDs:");
243 for uuid in cfg_uuids {
244 cli_println!(@Green, "{uuid}");
245 }
246 }
247
248 cli_print!("The remote Formation Instance URL is ");
249 cli_println!(@Green, "{domain}");
250 cli_println!(
251 "(hint: it may take up to a minute for the Formation to become fully online)"
252 );
253 if !has_public_endpoints {
254 cli_println!("(hint: there are no public endpoints configured, the Formation will not be reachable from the public internet)");
255 }
256 cli_print!("(hint: check the status of this Formation Instance with '");
257 cli_print!(@Green, "seaplane formation status {formation_name}");
258 cli_println!("')");
259 }
260
261 ctx.persist_formations()?;
262
263 Ok(())
264 }
265
266 fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
267 ctx.args.name_id = matches
268 .get_one::<String>("formation")
269 .map(ToOwned::to_owned);
270 ctx.args.fetch = matches.get_flag("fetch");
271 let fctx = ctx.formation_ctx.get_mut_or_init();
272 fctx.grounded = matches.get_flag("grounded");
273 Ok(())
274 }
275}