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