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
28impl 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 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}