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