tfc_toolset/
variable.rs

1use crate::{
2    build_request,
3    error::ToolError,
4    settings::Core,
5    workspace::{Workspace, WorkspaceVariables},
6    BASE_URL,
7};
8
9use log::{error, info};
10use serde::{Deserialize, Serialize};
11use serde_json::json;
12use std::{
13    fmt::{Display, Formatter},
14    str::FromStr,
15};
16use surf::{http::Method, Client};
17use url::Url;
18
19#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
20pub struct Variable {
21    #[serde(rename = "type")]
22    pub relationship_type: String,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub id: Option<String>,
25    pub attributes: Attributes,
26}
27
28// the vars are in the format of key=value:description:category:hcl:sensitive
29// we need to parse each one into a variable::Variable
30// description, category, hcl, sensitive are all optional and will be None if not provided
31// to skip a field just use a colon e.g. key=value::::true would only set key, value, and sensitive
32// accepting the default for the rest
33impl FromStr for Variable {
34    type Err = ToolError;
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        if s.contains(':') {
38            let var_split: Vec<&str> = s.split(':').collect();
39            let key_val = var_split[0].to_string();
40            let key_val_split: Vec<&str> = key_val.split('=').collect();
41            let key = key_val_split[0].to_string();
42            let value = key_val_split[1].to_string();
43            let description = if var_split[1].is_empty() {
44                None
45            } else {
46                Some(var_split[1].to_string())
47            };
48            let category = if var_split[2].is_empty() {
49                Category::default()
50            } else {
51                Category::from(var_split[2].to_string())
52            };
53            let hcl = if var_split[3].is_empty() {
54                None
55            } else {
56                Some(var_split[3].parse::<bool>()?)
57            };
58            let sensitive = if var_split[4].is_empty() {
59                None
60            } else {
61                Some(var_split[4].parse::<bool>()?)
62            };
63            Ok(Variable {
64                relationship_type: "vars".to_string(),
65                id: None,
66                attributes: Attributes {
67                    key,
68                    value: Some(value),
69                    description,
70                    category,
71                    hcl,
72                    sensitive,
73                },
74            })
75        } else {
76            let key_val_split = s.split('=').collect::<Vec<&str>>();
77            let key = key_val_split[0].to_string();
78            let value = key_val_split[1].to_string();
79            Ok(Variable {
80                relationship_type: "vars".to_string(),
81                id: None,
82                attributes: Attributes {
83                    key,
84                    value: Some(value),
85                    description: None,
86                    category: Category::default(),
87                    hcl: None,
88                    sensitive: None,
89                },
90            })
91        }
92    }
93}
94
95#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
96#[serde(rename_all = "snake_case")]
97pub enum Category {
98    #[default]
99    Terraform,
100    Env,
101}
102
103impl Display for Category {
104    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105        match self {
106            Category::Terraform => write!(f, "terraform"),
107            Category::Env => write!(f, "env"),
108        }
109    }
110}
111
112impl From<String> for Category {
113    fn from(s: String) -> Self {
114        match s.as_str() {
115            "terraform" => Category::Terraform,
116            "env" => Category::Env,
117            _ => Category::Terraform,
118        }
119    }
120}
121
122#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
123pub struct Attributes {
124    pub key: String,
125    pub value: Option<String>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub description: Option<String>,
128    #[serde(default)]
129    pub category: Category,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub hcl: Option<bool>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub sensitive: Option<bool>,
134}
135
136#[derive(Clone, Debug, Deserialize, Serialize)]
137pub struct VariablesOuter {
138    pub data: Vec<Variable>,
139}
140
141#[derive(Clone, Debug, Deserialize, Serialize)]
142struct VariableOuter {
143    pub data: Variable,
144}
145
146pub async fn create(
147    workspace_id: &str,
148    var: Variable,
149    config: &Core,
150    client: Client,
151) -> Result<Variable, ToolError> {
152    info!(
153        "Creating variable: {} in workspace: {}",
154        var.attributes.key, workspace_id
155    );
156    let url =
157        Url::parse(&format!("{}/workspaces/{}/vars/", BASE_URL, workspace_id))?;
158    let req = build_request(
159        Method::Post,
160        url,
161        config,
162        Some(json!(VariableOuter { data: var })),
163    );
164    match client.send(req).await {
165        Ok(mut res) => {
166            if res.status().is_success() {
167                let body: VariableOuter =
168                    res.body_json().await.map_err(|e| e.into_inner())?;
169                Ok(body.data)
170            } else {
171                error!("Failed to create variable :(");
172                let error =
173                    res.body_string().await.map_err(|e| e.into_inner())?;
174                Err(ToolError::General(anyhow::anyhow!(error)))
175            }
176        }
177        Err(e) => Err(ToolError::General(e.into_inner())),
178    }
179}
180pub async fn list(
181    workspace_id: &str,
182    config: &Core,
183    client: Client,
184) -> Result<Vec<Variable>, ToolError> {
185    let url =
186        Url::parse(&format!("{}/workspaces/{}/vars/", BASE_URL, workspace_id))?;
187    let req = build_request(Method::Get, url, config, None);
188    match client.send(req).await {
189        Ok(mut res) => {
190            if res.status().is_success() {
191                info!("Successfully retrieved variables!");
192                let body: VariablesOuter =
193                    res.body_json().await.map_err(|e| e.into_inner())?;
194                Ok(body.data)
195            } else {
196                error!("Failed to list variables :(");
197                let error =
198                    res.body_string().await.map_err(|e| e.into_inner())?;
199                Err(ToolError::General(anyhow::anyhow!(error)))
200            }
201        }
202        Err(e) => Err(ToolError::General(e.into_inner())),
203    }
204}
205
206pub async fn list_batch(
207    config: &Core,
208    client: Client,
209    workspaces: Vec<Workspace>,
210) -> Result<Vec<WorkspaceVariables>, ToolError> {
211    let mut workspaces_variables: Vec<WorkspaceVariables> = vec![];
212    // Get the variables for each workspace
213    for workspace in workspaces {
214        let variables = list(&workspace.id, config, client.clone()).await?;
215        workspaces_variables.push(WorkspaceVariables { workspace, variables });
216    }
217    Ok(workspaces_variables)
218}
219
220pub async fn delete(
221    variable_id: &str,
222    workspace_id: &str,
223    config: &Core,
224    client: Client,
225) -> Result<(), ToolError> {
226    info!(
227        "Deleting variable: {} from workspace: {}",
228        variable_id, workspace_id
229    );
230    let url = Url::parse(&format!(
231        "{}/workspaces/{}/vars/{}",
232        BASE_URL, workspace_id, variable_id
233    ))?;
234    let req = build_request(Method::Delete, url, config, None);
235    match client.send(req).await {
236        Ok(mut res) => {
237            if res.status().is_success() {
238                info!("Successfully deleted variable!");
239                Ok(())
240            } else {
241                error!("Failed to delete variable :(");
242                let error =
243                    res.body_string().await.map_err(|e| e.into_inner())?;
244                Err(ToolError::General(anyhow::anyhow!(error)))
245            }
246        }
247        Err(e) => Err(ToolError::General(e.into_inner())),
248    }
249}