1use std::collections::HashMap;
2
3use async_trait::async_trait;
4use serde::{
5 Deserialize,
6 Serialize,
7};
8
9use crate::error::PluginResult;
10use crate::health::{
11 ClientOptions,
12 ComponentHealth,
13 StatusSource,
14};
15use crate::logs::{
16 LogCursor,
17 LogDialect,
18 LogNode,
19 LogRead,
20 LogSink,
21 PlainDialect,
22 RunRef,
23};
24use crate::schema::{
25 ConfigSchema,
26 TableSchema,
27};
28use crate::sign_in::{
29 SignInClient,
30 SignInRefreshRejection,
31};
32use crate::types::*;
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PluginMetadata {
36 pub name: String,
37 pub provider_type: String,
38 pub version: String,
39 pub description: String,
40 pub author: Option<String>,
41 pub icon: Option<String>,
42 pub config_schema: ConfigSchema,
43 pub table_schema: TableSchema,
44 pub capabilities: PluginCapabilities,
45 #[serde(default)]
46 pub required_permissions: Vec<Permission>,
47 #[serde(default)]
48 pub features: Vec<Feature>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52pub struct PluginCapabilities {
53 pub pipelines: bool,
54 pub pipeline_runs: bool,
55 pub trigger: bool,
56 pub cancel: bool,
57 #[serde(default)]
58 pub force_cancel: bool,
59 pub agents: bool,
60 pub artifacts: bool,
61 pub queues: bool,
62 pub custom_tables: bool,
63 #[serde(default)]
64 pub retains_run_history: bool,
65 #[serde(default)]
66 pub logs: bool,
67 #[serde(default)]
68 pub sign_in: bool,
69}
70
71#[async_trait]
72pub trait Plugin: Send + Sync {
73 fn metadata(&self) -> &PluginMetadata;
74
75 fn initialize(
76 &mut self, provider_id: i64, config: HashMap<String, String>,
77 http_client: Option<std::sync::Arc<reqwest::Client>>,
78 ) -> PluginResult<()>;
79
80 fn client_options(&self, _config: &HashMap<String, String>) -> ClientOptions {
81 ClientOptions::default()
82 }
83
84 fn status_source(&self) -> Option<&StatusSource> {
85 None
86 }
87
88 async fn check_health(&self) -> PluginResult<Vec<ComponentHealth>> {
89 Ok(Vec::new())
90 }
91
92 async fn validate_credentials(&self) -> PluginResult<bool>;
93
94 async fn fetch_available_pipelines(
95 &self, params: Option<crate::types::PaginationParams>,
96 ) -> PluginResult<crate::types::PaginatedAvailablePipelines> {
97 let _ = params;
98 Ok(crate::types::PaginatedResponse::empty())
99 }
100
101 async fn fetch_organizations(&self) -> PluginResult<Vec<crate::types::Organization>> {
102 Ok(Vec::new())
103 }
104
105 async fn fetch_pipeline_catalog(&self) -> PluginResult<crate::types::PipelineCatalog> {
106 Err(crate::error::PluginError::NotSupported(
107 "Pipeline catalog not supported by this provider".to_string(),
108 ))
109 }
110
111 async fn ids_with_ci(&self, ids: Vec<String>) -> PluginResult<Vec<String>> {
112 Ok(ids)
113 }
114
115 fn log_dialect(&self) -> &'static dyn LogDialect {
116 &PlainDialect
117 }
118
119 async fn fetch_log_nodes(&self, _run: RunRef<'_>) -> PluginResult<Vec<LogNode>> {
120 Err(crate::error::PluginError::NotSupported(
121 "Run logs not supported by this provider".to_string(),
122 ))
123 }
124
125 async fn read_log(
126 &self, _run: RunRef<'_>, _node_id: &str, _from: Option<&LogCursor>, _sink: &mut LogSink,
127 ) -> PluginResult<LogRead> {
128 Err(crate::error::PluginError::NotSupported(
129 "Run logs not supported by this provider".to_string(),
130 ))
131 }
132
133 async fn fetch_available_pipelines_filtered(
134 &self, org: Option<String>, search: Option<String>,
135 params: Option<crate::types::PaginationParams>,
136 ) -> PluginResult<crate::types::PaginatedAvailablePipelines> {
137 let response = self.fetch_available_pipelines(params.clone()).await?;
138
139 let mut filtered_items = response.items;
140
141 if let Some(org_filter) = org {
142 filtered_items.retain(|p| p.organization.as_ref() == Some(&org_filter));
143 }
144
145 if let Some(search_term) = search {
146 let search_lower = search_term.to_lowercase();
147 filtered_items.retain(|p| {
148 p.name.to_lowercase().contains(&search_lower)
149 || p.id.to_lowercase().contains(&search_lower)
150 || p.description
151 .as_ref()
152 .is_some_and(|d| d.to_lowercase().contains(&search_lower))
153 });
154 }
155
156 let total_count = filtered_items.len();
157 let params = params.unwrap_or_default();
158
159 Ok(crate::types::PaginatedResponse::new(
160 filtered_items,
161 params.page,
162 params.page_size,
163 total_count,
164 ))
165 }
166
167 async fn fetch_pipelines(&self) -> PluginResult<Vec<Pipeline>>;
168
169 async fn fetch_pipelines_paginated(
170 &self, page: usize, page_size: usize,
171 ) -> PluginResult<crate::types::PaginatedResponse<Pipeline>> {
172 let all_pipelines = self.fetch_pipelines().await?;
173 let total_count = all_pipelines.len();
174 let start = (page - 1) * page_size;
175 let end = start + page_size;
176
177 let pipelines = if start < total_count {
178 all_pipelines[start..end.min(total_count)].to_vec()
179 } else {
180 Vec::new()
181 };
182
183 Ok(crate::types::PaginatedResponse::new(
184 pipelines,
185 page,
186 page_size,
187 total_count,
188 ))
189 }
190
191 async fn fetch_run_history(
192 &self, pipeline_id: &str, limit: usize,
193 ) -> PluginResult<Vec<PipelineRun>>;
194
195 async fn fetch_run_details(
196 &self, pipeline_id: &str, run_number: i64,
197 ) -> PluginResult<PipelineRun>;
198
199 async fn fetch_recent_runs(
200 &self, _pipeline_ids: &[String], _limit: usize,
201 ) -> PluginResult<HashMap<String, Vec<PipelineRun>>> {
202 Err(crate::error::PluginError::NotSupported(
203 "Batched run history not supported by this provider".to_string(),
204 ))
205 }
206
207 async fn trigger_pipeline(&self, params: TriggerParams) -> PluginResult<String>;
208
209 async fn cancel_run(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
210 Err(crate::error::PluginError::NotSupported(
211 "Run cancellation not supported by this provider".to_string(),
212 ))
213 }
214
215 async fn force_cancel_run(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
216 Err(crate::error::PluginError::NotSupported(
217 "Force cancellation not supported by this provider".to_string(),
218 ))
219 }
220
221 async fn review_run(
222 &self, _pipeline_id: &str, _run_number: i64, _decision: ReviewDecision,
223 ) -> PluginResult<()> {
224 Err(crate::error::PluginError::NotSupported(
225 "Run review not supported by this provider".to_string(),
226 ))
227 }
228
229 async fn rerun_failed_jobs(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
230 Err(crate::error::PluginError::NotSupported(
231 "Re-running failed jobs not supported by this provider".to_string(),
232 ))
233 }
234
235 async fn rerun_run(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
236 Err(crate::error::PluginError::NotSupported(
237 "Re-running runs not supported by this provider".to_string(),
238 ))
239 }
240
241 async fn enable_pipeline(&self, _pipeline_id: &str) -> PluginResult<()> {
242 Err(crate::error::PluginError::NotSupported(
243 "Enabling pipelines not supported by this provider".to_string(),
244 ))
245 }
246
247 async fn fetch_workflow_parameters(
248 &self, _workflow_id: &str,
249 ) -> PluginResult<Vec<WorkflowParameter>> {
250 Ok(Vec::new())
251 }
252
253 async fn fetch_agents(&self) -> PluginResult<Vec<BuildAgent>> {
254 Err(crate::error::PluginError::NotSupported(
255 "Agent monitoring not supported by this provider".to_string(),
256 ))
257 }
258
259 async fn fetch_artifacts(&self, _run_id: &str) -> PluginResult<Vec<BuildArtifact>> {
260 Err(crate::error::PluginError::NotSupported(
261 "Artifact fetching not supported by this provider".to_string(),
262 ))
263 }
264
265 async fn fetch_queues(&self) -> PluginResult<Vec<BuildQueue>> {
266 Err(crate::error::PluginError::NotSupported(
267 "Queue monitoring not supported by this provider".to_string(),
268 ))
269 }
270
271 async fn fetch_rate_limit(&self) -> PluginResult<Option<RateLimit>> {
272 Ok(None)
273 }
274
275 fn get_migrations(&self) -> Vec<String> {
276 Vec::new()
277 }
278
279 fn provider_type(&self) -> &str {
280 &self.metadata().provider_type
281 }
282
283 async fn get_field_options(
284 &self, _field_key: &str, _config: &HashMap<String, String>,
285 ) -> PluginResult<Vec<String>> {
286 Ok(Vec::new())
287 }
288
289 async fn sign_in_client(
290 &self, _config: &HashMap<String, String>, _http_client: &reqwest::Client,
291 ) -> PluginResult<SignInClient> {
292 Err(crate::error::PluginError::NotSupported(
293 "Sign-in is not supported by this provider".to_string(),
294 ))
295 }
296
297 fn sign_in_refresh_rejections(&self) -> &'static [SignInRefreshRejection] {
298 &[]
299 }
300
301 async fn check_permissions(&self) -> PluginResult<PermissionStatus> {
302 let required_permissions = &self.metadata().required_permissions;
303 let permissions = required_permissions
304 .iter()
305 .map(|p| PermissionCheck {
306 permission: p.clone(),
307 granted: true,
308 })
309 .collect();
310
311 Ok(PermissionStatus {
312 permissions,
313 all_granted: true,
314 checked_at: chrono::Utc::now(),
315 metadata: std::collections::HashMap::new(),
316 })
317 }
318
319 fn get_feature_availability(&self, status: &PermissionStatus) -> Vec<FeatureAvailability> {
320 let features = &self.metadata().features;
321 let granted_perms: std::collections::HashSet<String> = status
322 .permissions
323 .iter()
324 .filter(|p| p.granted)
325 .map(|p| p.permission.name.clone())
326 .collect();
327
328 features
329 .iter()
330 .map(|feature| {
331 let missing: Vec<String> = feature
332 .required_permissions
333 .iter()
334 .filter(|p| !granted_perms.contains(*p))
335 .cloned()
336 .collect();
337
338 FeatureAvailability {
339 feature: feature.clone(),
340 available: missing.is_empty(),
341 missing_permissions: missing,
342 }
343 })
344 .collect()
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn force_cancellation_defaults_to_unsupported_for_existing_capabilities() {
354 let capabilities = PluginCapabilities::default();
355 assert!(!capabilities.force_cancel);
356
357 let mut serialized = serde_json::to_value(capabilities).unwrap();
358 serialized.as_object_mut().unwrap().remove("force_cancel");
359 let restored: PluginCapabilities = serde_json::from_value(serialized).unwrap();
360 assert!(!restored.force_cancel);
361 }
362}