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