tfc_toolset/
variable_set.rs

1use crate::{
2    build_request,
3    error::{surf_to_tool_error, ToolError},
4    set_page_number,
5    settings::Core,
6    variable::{Variable, VariablesOuter},
7    workspace::Workspace,
8    Meta, BASE_URL,
9};
10
11use log::{error, info};
12use serde::{Deserialize, Serialize};
13use serde_json::json;
14use surf::{http::Method, Client};
15use url::Url;
16
17#[derive(Clone, Debug, Deserialize, Serialize)]
18pub struct Attributes {
19    pub name: String,
20    pub description: String,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub global: Option<bool>,
23}
24
25#[derive(Clone, Debug, Deserialize, Serialize)]
26pub struct Workspaces {
27    #[serde(rename = "type")]
28    pub relationship_type: String,
29    pub id: String,
30}
31
32#[derive(Clone, Debug, Deserialize, Serialize)]
33pub struct WorkspacesOuter {
34    pub data: Vec<Workspaces>,
35}
36
37impl From<Vec<Workspace>> for WorkspacesOuter {
38    fn from(workspaces: Vec<Workspace>) -> Self {
39        Self {
40            data: workspaces
41                .into_iter()
42                .map(|ws| Workspaces {
43                    relationship_type: "workspaces".to_string(),
44                    id: ws.id,
45                })
46                .collect(),
47        }
48    }
49}
50
51#[derive(Clone, Debug, Deserialize, Serialize)]
52pub struct Projects {
53    #[serde(rename = "type")]
54    pub relationship_type: String,
55    pub id: String,
56}
57
58#[derive(Clone, Debug, Deserialize, Serialize)]
59pub struct ProjectsOuter {
60    pub data: Vec<Projects>,
61}
62
63#[derive(Clone, Debug, Deserialize, Serialize)]
64pub struct Relationships {
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub workspaces: Option<WorkspacesOuter>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub projects: Option<ProjectsOuter>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub vars: Option<VariablesOuter>,
71}
72
73#[derive(Clone, Debug, Deserialize, Serialize)]
74pub struct VarSet {
75    #[serde(rename = "type")]
76    pub relationship_type: String,
77    pub attributes: Attributes,
78    pub relationships: Relationships,
79}
80
81#[derive(Clone, Debug, Serialize, Deserialize)]
82pub struct VarSets {
83    pub data: Vec<VarSet>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub meta: Option<Meta>,
86}
87
88impl VarSets {
89    pub fn new(var_sets: Vec<VarSet>) -> Self {
90        VarSets { data: var_sets, meta: None }
91    }
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize)]
95struct VarSetOuter {
96    pub data: VarSet,
97}
98
99#[derive(Clone, Debug, Serialize, Deserialize)]
100pub struct VarSetOptions {
101    pub name: String,
102    pub description: String,
103    pub global: Option<bool>,
104    pub workspaces: Option<Vec<Workspace>>,
105    pub projects: Option<Vec<String>>,
106    pub vars: Option<Vec<Variable>>,
107}
108
109impl VarSetOuter {
110    pub fn new(options: VarSetOptions) -> Self {
111        Self {
112            data: VarSet {
113                relationship_type: "vars".to_string(),
114                attributes: Attributes {
115                    name: options.name.to_string(),
116                    description: options.description.to_string(),
117                    global: options.global,
118                },
119                relationships: Relationships {
120                    workspaces: options.workspaces.map(|ws| WorkspacesOuter {
121                        data: ws
122                            .into_iter()
123                            .map(|ws| Workspaces {
124                                relationship_type: "workspaces".to_string(),
125                                id: ws.id,
126                            })
127                            .collect(),
128                    }),
129                    projects: options.projects.map(|p| ProjectsOuter {
130                        data: p
131                            .into_iter()
132                            .map(|p| Projects {
133                                relationship_type: "projects".to_string(),
134                                id: p,
135                            })
136                            .collect(),
137                    }),
138                    vars: options.vars.map(|v| VariablesOuter { data: v }),
139                },
140            },
141        }
142    }
143}
144
145#[derive(Clone, Debug, Serialize)]
146pub struct ApplyVarSet {
147    #[serde(rename = "type")]
148    pub relationship_type: String,
149    pub id: String,
150}
151
152#[derive(Clone, Debug, Serialize)]
153struct ApplyVarSetOuter {
154    pub data: Vec<ApplyVarSet>,
155}
156
157impl From<Vec<Workspace>> for ApplyVarSetOuter {
158    fn from(workspaces: Vec<Workspace>) -> Self {
159        Self {
160            data: workspaces
161                .into_iter()
162                .map(|ws| ApplyVarSet {
163                    relationship_type: "workspaces".to_string(),
164                    id: ws.id,
165                })
166                .collect(),
167        }
168    }
169}
170
171async fn check_pagination(
172    meta: Meta,
173    var_set_list: &mut VarSets,
174    url: Url,
175    config: &Core,
176    client: Client,
177) -> Result<(), ToolError> {
178    let max_depth = config.pagination.max_depth.parse::<u32>()?;
179    if max_depth > 1 || max_depth == 0 {
180        let current_depth: u32 = 1;
181        if let Some(next_page) = meta.pagination.next_page {
182            if max_depth == 0 || current_depth < max_depth {
183                let num_pages: u32 = if max_depth >= meta.pagination.total_pages
184                    || max_depth == 0
185                {
186                    meta.pagination.total_pages
187                } else {
188                    max_depth
189                };
190
191                // Get the next page and merge the result
192                for n in next_page..=num_pages {
193                    let u = url.clone();
194                    info!("Retrieving variable set page {}.", &n);
195                    let u = match set_page_number(n, u) {
196                        Some(u) => u,
197                        None => {
198                            error!("Failed to set page number.");
199                            return Err(ToolError::Pagination(
200                                "Failed to set page number.".to_string(),
201                            ));
202                        }
203                    };
204                    let req =
205                        build_request(Method::Get, u.clone(), config, None);
206                    let mut response =
207                        client.send(req).await.map_err(surf_to_tool_error)?;
208                    if response.status().is_success() {
209                        let var_set_pages: Result<VarSets, ToolError> =
210                            response
211                                .body_json()
212                                .await
213                                .map_err(surf_to_tool_error);
214                        match var_set_pages {
215                            Ok(mut t) => {
216                                var_set_list.data.append(&mut t.data);
217                            }
218                            Err(e) => {
219                                error!("{:#?}", e);
220                                return Err(e);
221                            }
222                        }
223                    }
224                }
225            }
226        }
227    }
228    Ok(())
229}
230
231pub async fn show(
232    variable_set_id: &str,
233    config: &Core,
234    client: Client,
235) -> Result<VarSet, ToolError> {
236    let url = Url::parse(&format!("{}/varsets/{}", BASE_URL, variable_set_id))?;
237    let req = build_request(Method::Get, url, config, None);
238    let mut res = client.send(req).await.map_err(surf_to_tool_error)?;
239    if res.status().is_success() {
240        let var_set: VarSetOuter =
241            res.body_json().await.map_err(surf_to_tool_error)?;
242        Ok(var_set.data)
243    } else {
244        error!("Failed to show variable set");
245        let error = res.body_string().await.map_err(surf_to_tool_error)?;
246        Err(ToolError::General(anyhow::anyhow!(error)))
247    }
248}
249
250pub async fn list_by_org(
251    config: &Core,
252    client: Client,
253) -> Result<VarSets, ToolError> {
254    info!(
255        "Retrieving the initial list of variable sets for org {}.",
256        config.org
257    );
258    let params = vec![
259        ("page[size]", config.pagination.page_size.clone()),
260        ("page[number]", config.pagination.start_page.clone()),
261    ];
262    let url = Url::parse_with_params(
263        &format!("{}/organizations/{}/varsets", BASE_URL, config.org),
264        &params,
265    )?;
266    let req = build_request(Method::Get, url.clone(), config, None);
267    let mut var_set_list: VarSets = match client.send(req).await {
268        Ok(mut res) => {
269            if res.status().is_success() {
270                info!("Variable sets for org {} retrieved.", config.org);
271                match res.body_json().await {
272                    Ok(t) => t,
273                    Err(e) => {
274                        error!("{:#?}", e);
275                        return Err(ToolError::General(anyhow::anyhow!(e)));
276                    }
277                }
278            } else {
279                error!("Failed to fetch variable sets for org {}.", config.org);
280                let error =
281                    res.body_string().await.map_err(surf_to_tool_error)?;
282                return Err(ToolError::General(anyhow::anyhow!(error)));
283            }
284        }
285        Err(e) => {
286            return Err(ToolError::General(anyhow::anyhow!(e)));
287        }
288    };
289    // Need to check pagination
290    if let Some(meta) = var_set_list.meta.clone() {
291        check_pagination(meta, &mut var_set_list, url, config, client).await?;
292    }
293    info!("Finished retrieving variable sets.");
294    Ok(var_set_list)
295}
296
297pub async fn list_by_project(
298    config: &Core,
299    client: Client,
300) -> Result<VarSets, ToolError> {
301    if config.project.clone().is_none() {
302        return Err(ToolError::General(anyhow::anyhow!(
303            "No project specified in config"
304        )));
305    }
306    let project_id = config.project.clone().unwrap();
307    info!(
308        "Retrieving the initial list of variable sets for project {}.",
309        project_id
310    );
311    let params = vec![
312        ("page[size]", config.pagination.page_size.clone()),
313        ("page[number]", config.pagination.start_page.clone()),
314    ];
315    let url = Url::parse_with_params(
316        &format!("{}/projects/{}/varsets", BASE_URL, project_id),
317        &params,
318    )?;
319    let req = build_request(Method::Get, url.clone(), config, None);
320    let mut var_set_list: VarSets = match client.send(req).await {
321        Ok(mut res) => {
322            if res.status().is_success() {
323                info!("Variable sets for project {} retrieved.", project_id);
324                match res.body_json().await {
325                    Ok(t) => t,
326                    Err(e) => {
327                        error!("{:#?}", e);
328                        return Err(ToolError::General(anyhow::anyhow!(e)));
329                    }
330                }
331            } else {
332                error!(
333                    "Failed to fetch variable sets for project {}.",
334                    project_id
335                );
336                let error =
337                    res.body_string().await.map_err(surf_to_tool_error)?;
338                return Err(ToolError::General(anyhow::anyhow!(error)));
339            }
340        }
341        Err(e) => {
342            return Err(ToolError::General(anyhow::anyhow!(e)));
343        }
344    };
345    // Need to check pagination
346    if let Some(meta) = var_set_list.meta.clone() {
347        check_pagination(meta, &mut var_set_list, url, config, client).await?;
348    }
349    info!("Finished retrieving variable sets.");
350    Ok(var_set_list)
351}
352
353pub async fn create(
354    options: VarSetOptions,
355    config: &Core,
356    client: Client,
357) -> Result<(), ToolError> {
358    let url = Url::parse(&format!(
359        "{}/organizations/{}/varsets",
360        BASE_URL, config.org
361    ))?;
362    let req = build_request(
363        Method::Post,
364        url,
365        config,
366        Some(json!(VarSetOuter::new(options))),
367    );
368    let mut res = client.send(req).await.map_err(surf_to_tool_error)?;
369    if res.status().is_success() {
370        info!("Successfully created variable set");
371    } else {
372        error!("Failed to create variable set");
373        let error = res.body_string().await.map_err(surf_to_tool_error)?;
374        return Err(ToolError::General(anyhow::anyhow!(error)));
375    }
376    Ok(())
377}
378
379pub async fn apply_workspace(
380    variable_set_id: &str,
381    workspaces: Vec<Workspace>,
382    config: &Core,
383    client: Client,
384) -> Result<(), ToolError> {
385    let url = Url::parse(&format!(
386        "{}/varsets/{}/relationships/workspaces",
387        BASE_URL, variable_set_id
388    ))?;
389    let req = build_request(
390        Method::Post,
391        url,
392        config,
393        Some(json!(ApplyVarSetOuter::from(workspaces))),
394    );
395    let mut res = client.send(req).await.map_err(surf_to_tool_error)?;
396    if res.status().is_success() {
397        info!("Successfully applied workspaces to variable set");
398    } else {
399        error!("Failed to apply workspaces to variable set");
400        let error = res.body_string().await.map_err(surf_to_tool_error)?;
401        return Err(ToolError::General(anyhow::anyhow!(error)));
402    }
403    Ok(())
404}
405
406pub async fn remove_workspace(
407    variable_set_id: &str,
408    workspaces: Vec<Workspace>,
409    config: &Core,
410    client: Client,
411) -> Result<(), ToolError> {
412    let url = Url::parse(&format!(
413        "{}/varsets/{}/relationships/workspaces",
414        BASE_URL, variable_set_id
415    ))?;
416    let req = build_request(
417        Method::Delete,
418        url,
419        config,
420        Some(json!(WorkspacesOuter::from(workspaces))),
421    );
422    let mut res = client.send(req).await.map_err(surf_to_tool_error)?;
423    if res.status().is_success() {
424        info!("Successfully removed workspace from variable set");
425    } else {
426        error!("Failed to remove workspace from variable set");
427        let error = res.body_string().await.map_err(surf_to_tool_error)?;
428        return Err(ToolError::General(anyhow::anyhow!(error)));
429    }
430    Ok(())
431}