1use clap::{builder::PossibleValue, value_parser, Arg};
8use seaplane::api::shared::v1::{Provider as ProviderModel, Region as RegionModel};
9
10use crate::cli::validator::{
11 validate_endpoint, validate_formation_name, validate_name_id, validate_name_id_path_inline,
12 validate_public_endpoint,
13};
14
15static LONG_NAME: &str =
16 "A human readable name for the Formation (must be unique within the tenant)
17
18Rules for a valid name are as follows:
19
20 - may only include 0-9, a-z, A-Z, and '-' (hyphen)
21 - hyphens ('-') may not be repeated (i.e. '--')
22 - no more than three (3) total hyphens
23 - the total length must be <= 27
24
25Some of these restrictions may be lifted in the future.";
26
27static LONG_AFFINITY: &str = "A Formation Instance that this Formation has an affinity for.
28
29This is a hint to the scheduler to place containers running in each of these
30formations \"close\" to eachother (for some version of close including but
31not limited to latency).
32
33Multiple items can be passed as a comma separated list, or by using the argument
34multiple times.";
35
36static LONG_CONNECTION: &str = "A Formation Instance that this Formation is connected to.
37
38Two formations can communicate over their formation endpoints (the endpoints configured via
39--formation-endpoints) if and only if both formations opt in to that connection (list
40each other in their connections map)
41
42Multiple items can be passed as a comma separated list, or by using the argument
43multiple times.";
44
45static LONG_PUBLIC_ENDPOINT: &str = r#"An endpoint that will publicly exposed on Instances of this Formation Plan
46
47Public Endpoints take the form '{ROUTE}={FLIGHT}:{PORT}'. Where
48
49ROUTE := An HTTP URL route
50FLIGHT := NAME or ID
51PORT := Network Port (0-65535)
52
53This describes which Flight and port should serve the HTTP traffic arriving at this Formation's
54domain URL using the specified route.
55
56For example, consider:
57
58$ seaplane formation edit Foo --public-endpoint /foo/bar=baz:1234
59
60Would mean, all HTTP traffic from the public internet hitting the route '/foo/bar' on the 'Foo'
61Formation's domain should be directed to this Formation's Flight named 'baz' on port '1234'
62
63In the future, support for other protocols such as 'tcp:port' or 'udp:port' may be added alongside
64'http' routes.
65
66Multiple items can be passed as a comma separated list, or by using the argument
67multiple times."#;
68
69static LONG_FORMATION_ENDPOINT: &str = r#"An endpoint that will only exposed privately by Instances of this Formation Plan (only exposed to other Formations)
70
71Formation Endpoints take the form '{PROTO}:{TARGET}={FLIGHT}:{PORT}'. Where
72
73PROTO := [http | https] | tcp | udp
74TARGET := ROUTE | PORT
75ROUTE := with PROTO http, and HTTP URL route, can be elided
76PORT := with PROTO tcp | PROTO udp a Network Port (0-65535)
77FLIGHT := NAME or ID
78PORT := Network Port (0-65535)
79
80This describes where traffic arriving at instances of this Formation's domains URL from the private network
81should be sent.
82
83For example, consider:
84
85$ seaplane formation edit Foo --formation-endpoint tcp:22=baz:2222
86
87Would mean, route all traffic arriving to the 'Foo' Formation's domain URL on TCP/22 from the
88private network to the the Formation's Flight named 'baz' on port '2222'. The PROTO of the incoming
89traffic will be used for the PROTO of the outgoing traffic to FLIGHT
90
91Note 'https' can be used interchangeably with 'http' for convenience sake. It does NOT however
92require the traffic actually be HTTPS. Here 'http' (or convenience 'https') simply means "Traffic
93using the HTTP" protocol.
94
95Multiple items can be passed as a comma separated list, or by using the argument
96multiple times."#;
97
98static LONG_FLIGHT_ENDPOINT: &str = r#"An endpoint that will only be exposed privately on Instances of this Formation Plan (only exposed to Flights within this same Formation Instance)
99
100Flight Endpoints take the form '{PROTO}:{TARGET}={FLIGHT}:{PORT}'. Where
101
102PROTO := [http | https] | tcp | udp
103TARGET := ROUTE | PORT
104ROUTE := with PROTO http, and HTTP URL route, can be elided
105PORT := with PROTO tcp | PROTO udp a Network Port (0-65535)
106FLIGHT := NAME or ID
107PORT := Network Port (0-65535)
108
109This describes where traffic arriving at this Formation's domain URL from within this Formation's
110private network should be sent.
111
112For example, consider:
113
114$ seaplane formation edit Foo --flight-endpoint udp:1234=baz:4321
115
116Would mean, route all traffic arriving to the 'Foo' Formation's domain URL on UDP/1234 from the
117Formation's private network to the the Formation's Flight named 'baz' on port '4321'. The PROTO of
118the incoming traffic will be used for the PROTO of the outgoing traffic to FLIGHT
119
120Note 'https' can be used interchangeably with 'http' for convenience sake. It does NOT however
121require the traffic actually be HTTPS. Here 'http' (or convenience 'https') simply means "Traffic
122using the HTTP" protocol.
123
124Multiple items can be passed as a comma separated list, or by using the argument
125multiple times."#;
126
127static LONG_REGION: &str =
128 "A region in which this Formation's Flights are allowed to run in (See REGION SPEC below)
129
130Multiple items can be passed as a comma separated list, or by using the argument
131multiple times.";
132
133static LONG_PROVIDER: &str = "A provider that this Formation's Flights are permitted to run on
134
135Multiple items can be passed as a comma separated list, or by using the argument
136multiple times.";
137
138static LONG_EXCLUDE_PROVIDER: &str =
139 "A provider that this Formation's Flights are *NOT* permitted to run on
140
141This will override any values given to --provider
142
143Multiple items can be passed as a comma separated list, or by using the argument
144multiple times.";
145
146static LONG_EXCLUDE_REGION: &str =
147 "A region in which this Formation's Flights are *NOT* allowed to run in (See REGION SPEC below)
148
149This will override any values given to --region
150
151Multiple items can be passed as a comma separated list, or by using the argument
152multiple times.";
153
154static LONG_FLIGHT: &str =
155 "A Flight Plan to include in this Formation in the form of ID|NAME|@path|@-|INLINE-SPEC (See FLIGHT SPEC below)
156
157Multiple items can be passed as a SEMICOLON (';') separated list or by using the argument multiple
158times. Note that when using the INLINE-SPEC it's usually easiest to only place one Flight Plan per
159--include-flight-plan argument
160
161$ seaplane formation plan \\
162 --include-flight-plan name=flight1,image=nginx:latest \\
163 --include-flight-plan name=flight2,image=hello:latest
164
165Which would create, and include, two Flight Plans (flight1, and flight2).";
166#[derive(Debug, Copy, Clone, PartialEq, Eq, strum::EnumString)]
170#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
171pub enum Provider {
172 Aws,
173 Azure,
174 DigitalOcean,
175 Equinix,
176 Gcp,
177 All,
178}
179
180impl clap::ValueEnum for Provider {
181 fn value_variants<'a>() -> &'a [Self] {
182 use Provider::*;
183 &[Aws, Azure, DigitalOcean, Equinix, Gcp, All]
184 }
185
186 fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
187 use Provider::*;
188 match self {
189 Aws => Some(PossibleValue::new("aws")),
190 Azure => Some(PossibleValue::new("azure")),
191 DigitalOcean => Some(PossibleValue::new("digitalocean")),
192 Equinix => Some(PossibleValue::new("equinix")),
193 Gcp => Some(PossibleValue::new("gcp")),
194 All => Some(PossibleValue::new("all")),
195 }
196 }
197
198 fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
199 input.parse().map_err(|e| format!("{e}"))
202 }
203}
204
205impl Provider {
206 pub fn into_model(&self) -> Option<ProviderModel> {
207 if self == &Provider::All {
208 None
209 } else {
210 Some(self.into())
211 }
212 }
213}
214
215#[allow(clippy::from_over_into)]
216impl<'a> Into<ProviderModel> for &'a Provider {
217 fn into(self) -> ProviderModel {
218 use Provider::*;
219 match self {
220 Aws => ProviderModel::AWS,
221 Azure => ProviderModel::Azure,
222 DigitalOcean => ProviderModel::DigitalOcean,
223 Equinix => ProviderModel::Equinix,
224 Gcp => ProviderModel::GCP,
225 All => {
226 panic!("Provider::All cannot be converted into seaplane::api::shared::v1::Provider")
227 }
228 }
229 }
230}
231
232#[allow(clippy::upper_case_acronyms)]
238#[derive(Debug, Copy, Clone, PartialEq, Eq, strum::EnumString)]
239#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
240pub enum Region {
241 XA,
242 Asia,
243 XC,
244 PRC,
245 PeoplesRepublicOfChina,
246 XE,
247 Europe,
248 EU,
249 XF,
250 Africa,
251 XN,
252 NorthAmerica,
253 NAmerica,
254 XO,
255 Oceania,
256 XQ,
257 Antarctica,
258 XS,
259 SAmerica,
260 SouthAmerica,
261 XU,
262 UK,
263 UnitedKingdom,
264 All,
265}
266
267impl clap::ValueEnum for Region {
268 fn value_variants<'a>() -> &'a [Self] {
269 use Region::*;
270 &[
271 XA, XC, XE, XF, XN, XO, XQ, XS, XU, All,
281 ]
282 }
283
284 fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
285 use Region::*;
286 match self {
287 XA => Some(PossibleValue::new("xa").alias("asia")),
288 Asia => None,
289 XC => Some(PossibleValue::new("xc").aliases([
290 "prc",
291 "china",
292 "peoples-republic-of-china",
293 "peoples_republic_of_china",
294 "peoplesrepublicofchina",
295 ])),
296 PRC => None,
297 PeoplesRepublicOfChina => None,
298 XE => Some(PossibleValue::new("xe").aliases(["europe", "eu"])),
299 Europe => None,
300 EU => None,
301 XF => Some(PossibleValue::new("xf").alias("africa")),
302 Africa => None,
303 XN => Some(PossibleValue::new("xn").aliases([
304 "namerica",
305 "northamerica",
306 "n-america",
307 "north-america",
308 "n_america",
309 "north_america",
310 ])),
311 NorthAmerica => None,
312 NAmerica => None,
313 XO => Some(PossibleValue::new("xo").alias("oceania")),
314 Oceania => None,
315 XQ => Some(PossibleValue::new("xq").alias("antarctica")),
316 Antarctica => None,
317 XS => Some(PossibleValue::new("xs").aliases([
318 "samerica",
319 "southamerica",
320 "s-america",
321 "south-america",
322 "s_america",
323 "south_america",
324 ])),
325 SAmerica => None,
326 SouthAmerica => None,
327 XU => Some(PossibleValue::new("xu").aliases([
328 "uk",
329 "unitedkingdom",
330 "united-kingdom",
331 "united_kingdom",
332 ])),
333 UK => None,
334 UnitedKingdom => None,
335 All => Some(PossibleValue::new("all")),
336 }
337 }
338
339 fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
340 input.parse().map_err(|e| format!("{e}"))
343 }
344}
345
346impl Region {
347 pub fn into_model(&self) -> Option<RegionModel> {
348 if self == &Region::All {
349 None
350 } else {
351 Some(self.into())
352 }
353 }
354}
355
356#[allow(clippy::from_over_into)]
357impl<'a> Into<RegionModel> for &'a Region {
358 fn into(self) -> RegionModel {
359 use Region::*;
360 match self {
361 XA | Asia => RegionModel::XA,
362 XC | PRC | PeoplesRepublicOfChina => RegionModel::XC,
363 XE | Europe | EU => RegionModel::XE,
364 XF | Africa => RegionModel::XF,
365 XN | NorthAmerica | NAmerica => RegionModel::XN,
366 XO | Oceania => RegionModel::XO,
367 XQ | Antarctica => RegionModel::XQ,
368 XS | SAmerica | SouthAmerica => RegionModel::XS,
369 XU | UK | UnitedKingdom => RegionModel::XU,
370 All => panic!("Region::All cannot be converted into seaplane::api::shared::v1::Region"),
371 }
372 }
373}
374
375pub fn args() -> Vec<Arg> {
376 let validator = |s: &str| validate_name_id(validate_formation_name, s);
377 #[cfg_attr(not(feature = "unstable"), allow(unused_mut))]
378 let mut hide = true;
379 let _ = hide;
380 #[cfg(feature = "unstable")]
381 {
382 hide = false;
383 }
384
385 vec![
387 arg!(name_id --name -('n') =["STRING"])
388 .help("A human readable name for the Formation (must be unique within the tenant) if omitted a pseudo random name will be assigned")
389 .long_help(LONG_NAME)
390 .value_parser(validate_formation_name),
391 arg!(--launch|active)
392 .overrides_with("launch") .help("This Formation Plan should be deployed and set as active right away (requires a formation configuration)"),
394 arg!(--grounded|("no-active"))
395 .help("This Formation Plan should be deployed but NOT set as active (requires a formation configuration)"),
396 arg!(--("include-flight-plan")|("include-flight-plans") -('I') =["SPEC"]...)
397 .help("Use local Flight Plan in this Formation in the form of ID|NAME|@path|@-|INLINE-SPEC (supports SEMICOLON (';') separated list, or multiple uses) (See FLIGHT SPEC below)")
398 .value_delimiter(';')
399 .required(true)
400 .long_help(LONG_FLIGHT)
401 .value_parser(validate_name_id_path_inline),
402 arg!(--provider|providers =["PROVIDER"=>"all"]... ignore_case)
403 .help("A provider that this Formation's Flights are permitted to run on (supports comma separated list, or multiple uses)")
404 .long_help(LONG_PROVIDER)
405 .value_parser(value_parser!(Provider)),
406 arg!(--("exclude-provider")|("exclude-providers") =["PROVIDER"]... ignore_case)
407 .help("A provider that this Formation's Flights are *NOT* permitted to run on (supports comma separated list, or multiple uses)")
408 .long_help(LONG_EXCLUDE_PROVIDER)
409 .value_parser(value_parser!(Provider)),
410 arg!(--region|regions =["REGION"=>"all"]... ignore_case)
411 .help("A region in which this Formation's Flights are allowed to run in (supports comma separated list, or multiple uses) (See REGION SPEC below)")
412 .long_help(LONG_REGION)
413 .value_parser(value_parser!(Region)),
414 arg!(--("exclude-region")|("exclude-regions") =["REGION"]... ignore_case)
415 .help("A region in which this Formation's Flights are *NOT* allowed to run in (supports comma separated list, or multiple uses) (See REGION SPEC below)")
416 .long_help(LONG_EXCLUDE_REGION)
417 .value_parser(value_parser!(Region)),
418 arg!(--("public-endpoint")|("public-endpoints") =["SPEC"]...)
420 .help("An endpoint that will be publicly exposed by instances of this Formation Plan in the form of 'ROUTE=FLIGHT:PORT' (supports comma separated list, or multiple uses)")
421 .long_help(LONG_PUBLIC_ENDPOINT)
422 .value_parser(validate_public_endpoint),
423 arg!(--("flight-endpoint")|("flight-endpoints") =["SPEC"]...)
425 .value_parser(validate_endpoint)
426 .help("An endpoint that will only be privately exposed on Instances of this Formation Plan to Flights within the same Formation Instance. In the form of 'PROTO:TARGET=FLIGHT:PORT' (supports comma separated list, or multiple uses)")
427 .long_help(LONG_FLIGHT_ENDPOINT),
428 arg!(--affinity|affinities =["NAME|ID"]...)
429 .help("A Formation that this Formation has an affinity for (supports comma separated list, or multiple uses)")
430 .long_help(LONG_AFFINITY)
431 .value_parser(validator)
432 .hide(hide), arg!(--connection|connections =["NAME|ID"]...)
434 .help("A Formations that this Formation is connected to (supports comma separated list, or multiple uses)")
435 .long_help(LONG_CONNECTION)
436 .value_parser(validator)
437 .hide(hide), arg!(--("formation-endpoint")|("formation-endpoints") =["SPEC"]...)
440 .value_parser(validate_endpoint)
441 .help("An endpoints that will only be exposed privately on Instances of this Formation Plan to other Formations within the same tenant and who have declared mutual connections. In the form of 'PROTO:TARGET=FLIGHT:PORT' (supports comma separated list, or multiple uses)")
442 .long_help(LONG_FORMATION_ENDPOINT)
443 .hide(hide), ]
445}