gitlab_manager/api/
variables.rs

1//! Project CI/CD variables API operations.
2//!
3//! This module provides operations for managing CI/CD variables
4//! at the project level.
5
6use crate::error::{GitLabError, Result};
7use crate::models::{Variable, VariableOptions, VariableType};
8use gitlab::api::{projects::variables, ApiError, Query};
9use gitlab::Gitlab;
10
11/// Builder for managing project CI/CD variables.
12///
13/// Provides operations to create, update, list, and delete
14/// CI/CD variables for a GitLab project.
15///
16/// # Examples
17///
18/// ```no_run
19/// use gitlab_manager::{GitLabClient, models::VariableOptions};
20///
21/// # async fn example() -> Result<(), gitlab_manager::GitLabError> {
22/// let client = GitLabClient::new("https://gitlab.com", "token")?;
23///
24/// // Create a new variable
25/// let opts = VariableOptions::new()
26///     .protected(true)
27///     .masked(true);
28///
29/// client.variables("myorg/myproject")
30///     .create("API_KEY", "secret-value", opts)
31///     .await?;
32/// # Ok(())
33/// # }
34/// ```
35pub struct VariableBuilder<'a> {
36    client: &'a Gitlab,
37    project: String,
38}
39
40impl<'a> VariableBuilder<'a> {
41    /// Creates a new variable builder.
42    pub(crate) fn new(client: &'a Gitlab, project: impl Into<String>) -> Self {
43        Self {
44            client,
45            project: project.into(),
46        }
47    }
48
49    /// Create a new CI/CD variable.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the variable already exists or if the request fails.
54    ///
55    /// # Examples
56    ///
57    /// ```no_run
58    /// # use gitlab_manager::{GitLabClient, models::VariableOptions};
59    /// # async fn example() -> Result<(), gitlab_manager::GitLabError> {
60    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
61    /// let opts = VariableOptions::new()
62    ///     .protected(true)
63    ///     .masked(true);
64    ///
65    /// client.variables("myproject")
66    ///     .create("DATABASE_URL", "postgres://...", opts)
67    ///     .await?;
68    /// # Ok(())
69    /// # }
70    /// ```
71    pub async fn create(
72        &self,
73        key: impl AsRef<str>,
74        value: impl AsRef<str>,
75        opts: VariableOptions,
76    ) -> Result<Variable> {
77        let key = key.as_ref();
78        let value = value.as_ref();
79
80        // Build the request using the gitlab crate's API
81        let mut builder = variables::CreateProjectVariable::builder();
82        builder
83            .project(&self.project)
84            .key(key)
85            .value(value)
86            .variable_type(match opts.variable_type {
87                VariableType::EnvVar => variables::ProjectVariableType::EnvVar,
88                VariableType::File => variables::ProjectVariableType::File,
89            })
90            .protected(opts.protected)
91            .masked(opts.masked)
92            .raw(opts.raw);
93
94        if let Some(scope) = opts.environment_scope {
95            builder.environment_scope(scope);
96        }
97
98        if let Some(desc) = opts.description {
99            builder.description(desc);
100        }
101
102        let endpoint = builder
103            .build()
104            .map_err(|e| GitLabError::api(format!("Failed to build create variable request: {}", e)))?;
105
106        endpoint
107            .query(self.client)
108            .map_err(|e| match e {
109                ApiError::GitlabService { status, .. } if status.as_u16() == 400 => {
110                    GitLabError::conflict(format!("Variable '{}' already exists", key))
111                }
112                _ => GitLabError::api(format!("Failed to create variable '{}': {}", key, e)),
113            })
114    }
115
116    /// Update an existing CI/CD variable.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the variable doesn't exist or if the request fails.
121    ///
122    /// # Examples
123    ///
124    /// ```no_run
125    /// # use gitlab_manager::{GitLabClient, models::VariableOptions};
126    /// # async fn example() -> Result<(), gitlab_manager::GitLabError> {
127    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
128    /// let opts = VariableOptions::new()
129    ///     .protected(true);
130    ///
131    /// client.variables("myproject")
132    ///     .update("API_KEY", "new-secret-value", opts)
133    ///     .await?;
134    /// # Ok(())
135    /// # }
136    /// ```
137    pub async fn update(
138        &self,
139        key: impl AsRef<str>,
140        value: impl AsRef<str>,
141        opts: VariableOptions,
142    ) -> Result<Variable> {
143        let key = key.as_ref();
144        let value = value.as_ref();
145
146        let mut builder = variables::UpdateProjectVariable::builder();
147        builder
148            .project(&self.project)
149            .key(key)
150            .value(value)
151            .variable_type(match opts.variable_type {
152                VariableType::EnvVar => variables::ProjectVariableType::EnvVar,
153                VariableType::File => variables::ProjectVariableType::File,
154            })
155            .protected(opts.protected)
156            .masked(opts.masked)
157            .raw(opts.raw);
158
159        if let Some(scope) = opts.environment_scope {
160            builder.environment_scope(scope);
161        }
162
163        if let Some(desc) = opts.description {
164            builder.description(desc);
165        }
166
167        let endpoint = builder
168            .build()
169            .map_err(|e| GitLabError::api(format!("Failed to build update variable request: {}", e)))?;
170
171        endpoint.query(self.client).map_err(|e| match e {
172            ApiError::GitlabService { status, .. } if status.as_u16() == 404 => {
173                GitLabError::not_found("Variable", key)
174            }
175            _ => GitLabError::api(format!("Failed to update variable '{}': {}", key, e)),
176        })
177    }
178
179    /// Create or update a CI/CD variable (upsert).
180    ///
181    /// If the variable exists, it will be updated. Otherwise, it will be created.
182    ///
183    /// # Examples
184    ///
185    /// ```no_run
186    /// # use gitlab_manager::{GitLabClient, models::VariableOptions};
187    /// # async fn example() -> Result<(), gitlab_manager::GitLabError> {
188    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
189    /// let opts = VariableOptions::new()
190    ///     .protected(true)
191    ///     .masked(true);
192    ///
193    /// // Will create if doesn't exist, update if it does
194    /// client.variables("myproject")
195    ///     .set("API_KEY", "secret-value", opts)
196    ///     .await?;
197    /// # Ok(())
198    /// # }
199    /// ```
200    pub async fn set(
201        &self,
202        key: impl AsRef<str>,
203        value: impl AsRef<str>,
204        opts: VariableOptions,
205    ) -> Result<Variable> {
206        let key = key.as_ref();
207
208        // Try to update first
209        match self.update(key, value.as_ref(), opts.clone()).await {
210            Ok(var) => Ok(var),
211            Err(GitLabError::NotFound { .. }) => {
212                // Variable doesn't exist, create it
213                self.create(key, value.as_ref(), opts).await
214            }
215            Err(e) => Err(e),
216        }
217    }
218
219    /// Get a CI/CD variable.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if the variable doesn't exist or if the request fails.
224    ///
225    /// # Examples
226    ///
227    /// ```no_run
228    /// # use gitlab_manager::GitLabClient;
229    /// # async fn example() -> Result<(), gitlab_manager::GitLabError> {
230    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
231    /// let var = client.variables("myproject")
232    ///     .get("API_KEY")
233    ///     .await?;
234    ///
235    /// println!("Value: {}", var.value);
236    /// # Ok(())
237    /// # }
238    /// ```
239    pub async fn get(&self, key: impl AsRef<str>) -> Result<Variable> {
240        let key = key.as_ref();
241
242        let endpoint = variables::ProjectVariable::builder()
243            .project(&self.project)
244            .key(key)
245            .build()
246            .map_err(|e| GitLabError::api(format!("Failed to build get variable request: {}", e)))?;
247
248        endpoint.query(self.client).map_err(|e| match e {
249            ApiError::GitlabService { status, .. } if status.as_u16() == 404 => {
250                GitLabError::not_found("Variable", key)
251            }
252            _ => GitLabError::api(format!("Failed to get variable '{}': {}", key, e)),
253        })
254    }
255
256    /// List all CI/CD variables for the project.
257    ///
258    /// # Examples
259    ///
260    /// ```no_run
261    /// # use gitlab_manager::GitLabClient;
262    /// # async fn example() -> Result<(), gitlab_manager::GitLabError> {
263    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
264    /// let vars = client.variables("myproject")
265    ///     .list()
266    ///     .await?;
267    ///
268    /// for var in vars {
269    ///     println!("{}: {} (protected={})", var.key, var.value, var.protected);
270    /// }
271    /// # Ok(())
272    /// # }
273    /// ```
274    pub async fn list(&self) -> Result<Vec<Variable>> {
275        let endpoint = variables::ProjectVariables::builder()
276            .project(&self.project)
277            .build()
278            .map_err(|e| GitLabError::api(format!("Failed to build list variables request: {}", e)))?;
279
280        endpoint
281            .query(self.client)
282            .map_err(|e| GitLabError::api(format!("Failed to list variables: {}", e)))
283    }
284
285    /// Delete a CI/CD variable.
286    ///
287    /// # Errors
288    ///
289    /// Returns an error if the variable doesn't exist or if the request fails.
290    ///
291    /// # Examples
292    ///
293    /// ```no_run
294    /// # use gitlab_manager::GitLabClient;
295    /// # async fn example() -> Result<(), gitlab_manager::GitLabError> {
296    /// # let client = GitLabClient::new("https://gitlab.com", "token")?;
297    /// client.variables("myproject")
298    ///     .delete("OLD_API_KEY")
299    ///     .await?;
300    /// # Ok(())
301    /// # }
302    /// ```
303    pub async fn delete(&self, key: impl AsRef<str>) -> Result<()> {
304        let key = key.as_ref();
305
306        let endpoint = variables::DeleteProjectVariable::builder()
307            .project(&self.project)
308            .key(key)
309            .build()
310            .map_err(|e| GitLabError::api(format!("Failed to build delete variable request: {}", e)))?;
311
312        // Delete endpoints don't return a value, use ignore() method
313        gitlab::api::ignore(endpoint)
314            .query(self.client)
315            .map_err(|e| match e {
316                ApiError::GitlabService { status, .. } if status.as_u16() == 404 => {
317                    GitLabError::not_found("Variable", key)
318                }
319                _ => GitLabError::api(format!("Failed to delete variable '{}': {}", key, e)),
320            })?;
321
322        Ok(())
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_variable_builder() {
332        fn _compile_test(client: &Gitlab) {
333            let _builder = VariableBuilder::new(client, "project");
334        }
335    }
336}