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    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}