Skip to main content

pipedash_plugin_api/
plugin.rs

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}