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