1use async_trait::async_trait;
10use crate::{GitLabClient, GitLabError, models};
11use lmrc_ports::{
12 CiVariable, CiVariableRequest, GitProvider, PipelineRun, PortError, PortResult, Repository,
13};
14
15pub struct GitLabAdapter {
17 client: GitLabClient,
18 url: String,
19}
20
21impl GitLabAdapter {
22 pub fn new(url: &str, token: &str) -> PortResult<Self> {
33 let client = GitLabClient::new(url, token)
34 .map_err(|e| PortError::InvalidConfiguration(format!("Failed to create GitLab client: {}", e)))?;
35
36 Ok(Self {
37 client,
38 url: url.to_string(),
39 })
40 }
41
42 pub fn from_env() -> PortResult<Self> {
52 let url = std::env::var("GITLAB_URL").unwrap_or_else(|_| "https://gitlab.com".to_string());
53
54 let token = std::env::var("GITLAB_TOKEN").map_err(|_| {
55 PortError::InvalidConfiguration(
56 "GITLAB_TOKEN environment variable is required".to_string(),
57 )
58 })?;
59
60 Self::new(&url, &token)
61 }
62}
63
64fn convert_error(err: GitLabError) -> PortError {
66 match err {
67 GitLabError::Authentication(msg) => {
68 PortError::InvalidConfiguration(format!("GitLab authentication failed: {}", msg))
69 }
70 GitLabError::NotFound { resource, id } => PortError::NotFound {
71 resource_type: resource,
72 resource_id: id,
73 },
74 GitLabError::Api(msg) => PortError::OperationFailed(format!("GitLab API error: {}", msg)),
75 GitLabError::Http(e) => PortError::NetworkError(format!("HTTP error: {}", e)),
76 GitLabError::Serialization(e) => {
77 PortError::OperationFailed(format!("Serialization error: {}", e))
78 }
79 GitLabError::RateLimit { retry_after } => PortError::OperationFailed(format!(
80 "Rate limit exceeded, retry after: {:?} seconds",
81 retry_after
82 )),
83 GitLabError::PermissionDenied(msg) => {
84 PortError::OperationFailed(format!("Permission denied: {}", msg))
85 }
86 GitLabError::InvalidInput { field, message } => {
87 PortError::InvalidConfiguration(format!("Invalid input for {}: {}", field, message))
88 }
89 GitLabError::Timeout { seconds } => {
90 PortError::OperationFailed(format!("Request timed out after {} seconds", seconds))
91 }
92 GitLabError::Conflict(msg) => PortError::AlreadyExists {
93 resource_type: "Resource".to_string(),
94 resource_id: msg,
95 },
96 GitLabError::ServerError(msg) => {
97 PortError::OperationFailed(format!("GitLab server error: {}", msg))
98 }
99 GitLabError::Unexpected(msg) => PortError::OperationFailed(format!("Unexpected error: {}", msg)),
100 GitLabError::Config(msg) => PortError::InvalidConfiguration(msg),
101 }
102}
103
104fn convert_to_port_variable(var: models::Variable) -> CiVariable {
106 CiVariable {
107 key: var.key,
108 value: var.value,
109 protected: var.protected,
110 masked: var.masked,
111 }
112}
113
114fn convert_variable_request(request: &CiVariableRequest) -> models::VariableOptions {
116 models::VariableOptions::new()
117 .protected(request.protected)
118 .masked(request.masked)
119}
120
121fn convert_pipeline_status(status: models::PipelineStatus) -> lmrc_ports::PipelineStatus {
123 use lmrc_ports::PipelineStatus;
124 match status {
125 models::PipelineStatus::Pending => PipelineStatus::Pending,
126 models::PipelineStatus::Running => PipelineStatus::Running,
127 models::PipelineStatus::Success => PipelineStatus::Success,
128 models::PipelineStatus::Failed => PipelineStatus::Failed,
129 models::PipelineStatus::Canceled => PipelineStatus::Canceled,
130 models::PipelineStatus::Skipped => PipelineStatus::Skipped,
131 _ => PipelineStatus::Failed, }
133}
134
135#[async_trait]
136impl GitProvider for GitLabAdapter {
137 async fn get_repository(&self, project_id: &str) -> PortResult<Repository> {
138 Ok(Repository {
142 id: project_id.to_string(),
143 name: project_id.split('/').next_back().unwrap_or(project_id).to_string(),
144 url: format!("{}/{}", self.url, project_id),
145 ssh_url: format!("git@{}:{}.git",
146 self.url.trim_start_matches("https://"),
147 project_id
148 ),
149 default_branch: "main".to_string(), })
151 }
152
153 async fn create_ci_variable(
154 &self,
155 project_id: &str,
156 request: CiVariableRequest,
157 ) -> PortResult<CiVariable> {
158 let opts = convert_variable_request(&request);
159
160 let var = self
161 .client
162 .variables(project_id)
163 .create(&request.key, &request.value, opts)
164 .await
165 .map_err(convert_error)?;
166
167 Ok(convert_to_port_variable(var))
168 }
169
170 async fn update_ci_variable(
171 &self,
172 project_id: &str,
173 key: &str,
174 request: CiVariableRequest,
175 ) -> PortResult<CiVariable> {
176 let opts = convert_variable_request(&request);
177
178 let var = self
179 .client
180 .variables(project_id)
181 .update(key, &request.value, opts)
182 .await
183 .map_err(convert_error)?;
184
185 Ok(convert_to_port_variable(var))
186 }
187
188 async fn list_ci_variables(&self, project_id: &str) -> PortResult<Vec<CiVariable>> {
189 let vars = self
190 .client
191 .variables(project_id)
192 .list()
193 .await
194 .map_err(convert_error)?;
195
196 Ok(vars.into_iter().map(convert_to_port_variable).collect())
197 }
198
199 async fn delete_ci_variable(&self, project_id: &str, key: &str) -> PortResult<()> {
200 self.client
201 .variables(project_id)
202 .delete(key)
203 .await
204 .map_err(convert_error)?;
205
206 Ok(())
207 }
208
209 async fn trigger_pipeline(
210 &self,
211 project_id: &str,
212 reference: &str,
213 ) -> PortResult<PipelineRun> {
214 let pipeline = self
215 .client
216 .project(project_id)
217 .create_pipeline()
218 .ref_name(reference)
219 .trigger()
220 .await
221 .map_err(convert_error)?;
222
223 Ok(PipelineRun {
224 id: pipeline.id.to_string(),
225 status: convert_pipeline_status(pipeline.status),
226 reference: pipeline.ref_name,
227 web_url: pipeline.web_url,
228 })
229 }
230
231 async fn get_pipeline(
232 &self,
233 project_id: &str,
234 pipeline_id: &str,
235 ) -> PortResult<PipelineRun> {
236 let pipeline_id_num = pipeline_id.parse::<u64>().map_err(|_| {
237 PortError::InvalidConfiguration(format!("Invalid pipeline ID: {}", pipeline_id))
238 })?;
239
240 let pipeline = self
241 .client
242 .pipeline(project_id, pipeline_id_num)
243 .get()
244 .await
245 .map_err(convert_error)?;
246
247 Ok(PipelineRun {
248 id: pipeline.id.to_string(),
249 status: convert_pipeline_status(pipeline.status),
250 reference: pipeline.ref_name,
251 web_url: pipeline.web_url,
252 })
253 }
254}