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