seaplane_cli/api/
formations.rs

1use reqwest::Url;
2use seaplane::{
3    api::{
4        compute::v1::{
5            ActiveConfigurations as ActiveConfigurationsModel, Container as ContainerModel,
6            Containers as ContainersModel, FormationConfiguration as FormationConfigurationModel,
7            FormationMetadata as FormationMetadataModel, FormationNames as FormationNamesModel,
8            FormationsRequest,
9        },
10        identity::v0::AccessToken,
11        ApiErrorKind,
12    },
13    error::SeaplaneError,
14};
15use uuid::Uuid;
16
17use crate::{
18    api::request_token,
19    context::Ctx,
20    error::{CliError, Context, Result},
21    ops::formation::{Formation, FormationConfiguration, Formations},
22    printer::{Color, Pb},
23};
24
25/// Wraps an SDK `FormationsRequest` where we do additional things like re-use request access
26/// tokens, allow changing the Formation this request is pointed to, and map errors appropriately.
27#[derive(Debug)]
28pub struct FormationsReq {
29    api_key: String,
30    name: Option<String>,
31    token: Option<AccessToken>,
32    inner: Option<FormationsRequest>,
33    identity_url: Option<Url>,
34    compute_url: Option<Url>,
35    insecure_urls: bool,
36    invalid_certs: bool,
37}
38
39impl FormationsReq {
40    /// Builds a FormationsRequest and immediately requests an access token using the given API key.
41    ///
42    /// If the `name` is `None` it should be noted that the only request that can be made without
43    /// error is `FormationsRequest::list_names`
44    pub fn new<S: Into<String>>(ctx: &Ctx, name: Option<S>) -> Result<Self> {
45        let mut this = Self::new_delay_token(ctx)?;
46        this.name = name.map(Into::into);
47        this.refresh_token()?;
48        Ok(this)
49    }
50
51    /// Builds a FormationsRequest but *does not* request an access token using the given API key.
52    ///
53    /// You must call `refresh_token` to have the access token requested.
54    pub fn new_delay_token(ctx: &Ctx) -> Result<Self> {
55        Ok(Self {
56            api_key: ctx.args.api_key()?.into(),
57            name: None,
58            token: None,
59            inner: None,
60            identity_url: ctx.identity_url.clone(),
61            compute_url: ctx.compute_url.clone(),
62            #[cfg(feature = "allow_insecure_urls")]
63            insecure_urls: ctx.insecure_urls,
64            #[cfg(not(feature = "allow_insecure_urls"))]
65            insecure_urls: false,
66            #[cfg(feature = "allow_invalid_certs")]
67            invalid_certs: ctx.invalid_certs,
68            #[cfg(not(feature = "allow_invalid_certs"))]
69            invalid_certs: false,
70        })
71    }
72
73    /// Request a new Access Token
74    pub fn refresh_token(&mut self) -> Result<()> {
75        self.token = Some(request_token(
76            &self.api_key,
77            self.identity_url.as_ref(),
78            self.insecure_urls,
79            self.invalid_certs,
80        )?);
81        Ok(())
82    }
83
84    /// Re-build the inner `FormationsRequest`. This is mostly useful when one wants to point at a
85    /// different Formation than the original request was pointed at (i.e. via `set_name`). This
86    /// method will also refresh the access token, only if required.
87    fn refresh_inner(&mut self) -> Result<()> {
88        let mut builder = FormationsRequest::builder().token(self.token_or_refresh()?);
89
90        #[cfg(feature = "allow_insecure_urls")]
91        {
92            builder = builder.allow_http(self.insecure_urls);
93        }
94        #[cfg(feature = "allow_invalid_certs")]
95        {
96            builder = builder.allow_invalid_certs(self.invalid_certs);
97        }
98
99        if let Some(url) = &self.compute_url {
100            builder = builder.base_url(url);
101        }
102
103        if let Some(name) = &self.name {
104            builder = builder.name(name);
105        }
106
107        self.inner = Some(builder.build().map_err(CliError::from)?);
108        Ok(())
109    }
110
111    /// Retrieves the JWT access token, requesting a new one if required.
112    pub fn token_or_refresh(&mut self) -> Result<&str> {
113        if self.token.is_none() {
114            self.refresh_token()?;
115        }
116        Ok(&self.token.as_ref().unwrap().token)
117    }
118
119    /// Sets the Formation name and re-builds the inner FormationsRequest also requesting a new
120    /// access token if required
121    pub fn set_name<S: Into<String>>(&mut self, name: S) -> Result<()> {
122        self.name = Some(name.into());
123        self.refresh_inner()
124    }
125
126    /// Retrieves all Formations and their Formation Configurations (both active and inactive) from
127    /// the Compute API. Makes multiple calls against the API to gather the info and returns a
128    /// `Formations` struct.
129    ///
130    /// It should be noted that the local IDs associated with all the items in the Formations
131    /// struct are generated unique after retrieval from the compute API. i.e. they do not match
132    /// anything existing in the local DB even if the contents are otherwise identical.
133    pub fn get_all_formations<S: AsRef<str>>(
134        &mut self,
135        formation_names: &[S],
136        pb: &Pb,
137    ) -> Result<Formations> {
138        let mut formations = Formations::default();
139        for name in formation_names {
140            let name = name.as_ref();
141            self.set_name(name)?;
142            pb.set_message(format!("Syncing Formation {name}..."));
143            let mut formation = Formation::new(name);
144
145            let cfg_uuids = self
146                .list_configuration_ids()
147                .context("Context: failed to retrieve Formation Configuration IDs\n")?;
148            let active_cfgs = self
149                .get_active_configurations()
150                .context("Context: failed to retrieve Active Formation Configurations\n")?;
151
152            pb.set_message(format!("Syncing Formation {name} Configurations..."));
153            for uuid in cfg_uuids.into_iter() {
154                let cfg_model = self
155                    .get_configuration(uuid)
156                    .context("Context: failed to retrieve Formation Configuration\n\tUUID: ")
157                    .with_color_context(|| (Color::Yellow, format!("{uuid}\n")))?;
158
159                let cfg = FormationConfiguration::with_uuid(uuid, cfg_model);
160                let is_active = active_cfgs.iter().any(|ac| ac.uuid() == &uuid);
161                formation.local.insert(cfg.id);
162                if is_active {
163                    formation.in_air.insert(cfg.id);
164                } else {
165                    formation.grounded.insert(cfg.id);
166                }
167                formations.configurations.push(cfg);
168            }
169
170            if !formation.is_empty() {
171                formations.formations.push(formation);
172            }
173        }
174
175        Ok(formations)
176    }
177
178    /// Return a `Vec` of all known formation names if this `FormationsReq` currently has no `name`
179    /// associated with it. Otherwise it returns the single `name` associated with this
180    /// `FormationsReq` (returned in a `Vec`). This is used when the CLI supports either doing
181    /// something to all formations, or just a single one that is passed in by the user.
182    pub fn get_formation_names(&mut self) -> Result<Vec<String>> {
183        Ok(if let Some(name) = &self.name {
184            vec![name.to_owned()]
185        } else {
186            // First download all formation names
187            self.list_names()
188                .context("Context: failed to retrieve Formation Instance names\n")?
189                .into_inner()
190        })
191    }
192}
193
194// Wrapped FormationsRequest methods to handle expired token retries
195impl FormationsReq {
196    pub fn list_names(&mut self) -> Result<FormationNamesModel> { maybe_retry!(self.list_names()) }
197
198    pub fn get_metadata(&mut self) -> Result<FormationMetadataModel> {
199        maybe_retry!(self.get_metadata())
200    }
201    pub fn create(
202        &mut self,
203        configuration: &FormationConfigurationModel,
204        active: bool,
205    ) -> Result<Vec<Uuid>> {
206        maybe_retry!(self.create(configuration, active))
207    }
208    pub fn clone_from(&mut self, source_name: &str, active: bool) -> Result<Vec<Uuid>> {
209        maybe_retry!(self.clone_from(source_name, active))
210    }
211    pub fn delete(&mut self, force: bool) -> Result<Vec<Uuid>> { maybe_retry!(self.delete(force)) }
212    pub fn get_active_configurations(&mut self) -> Result<ActiveConfigurationsModel> {
213        maybe_retry!(self.get_active_configurations())
214    }
215    pub fn stop(&mut self) -> Result<()> { maybe_retry!(self.stop()) }
216    pub fn set_active_configurations(
217        &mut self,
218        configs: &ActiveConfigurationsModel,
219        force: bool,
220    ) -> Result<()> {
221        maybe_retry!(self.set_active_configurations(configs, force))
222    }
223    pub fn get_containers(&mut self) -> Result<ContainersModel> {
224        maybe_retry!(self.get_containers())
225    }
226    pub fn get_container(&mut self, container_id: Uuid) -> Result<ContainerModel> {
227        maybe_retry!(self.get_container(container_id))
228    }
229    pub fn get_configuration(&mut self, uuid: Uuid) -> Result<FormationConfigurationModel> {
230        maybe_retry!(self.get_configuration(uuid))
231    }
232    pub fn list_configuration_ids(&mut self) -> Result<Vec<Uuid>> {
233        maybe_retry!(self.list_configuration_ids())
234    }
235    pub fn remove_configuration(&mut self, uuid: Uuid, force: bool) -> Result<Uuid> {
236        maybe_retry!(self.remove_configuration(uuid, force))
237    }
238    pub fn add_configuration(
239        &mut self,
240        configuration: &FormationConfigurationModel,
241        active: bool,
242    ) -> Result<Uuid> {
243        maybe_retry!(self.add_configuration(configuration, active))
244    }
245}