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 ids_with_ci(&self, ids: Vec<String>) -> PluginResult<Vec<String>> {
101        Ok(ids)
102    }
103
104    fn log_dialect(&self) -> &'static dyn LogDialect {
105        &PlainDialect
106    }
107
108    async fn fetch_log_nodes(&self, _run: RunRef<'_>) -> PluginResult<Vec<LogNode>> {
109        Err(crate::error::PluginError::NotSupported(
110            "Run logs not supported by this provider".to_string(),
111        ))
112    }
113
114    async fn read_log(
115        &self, _run: RunRef<'_>, _node_id: &str, _from: Option<&LogCursor>, _sink: &mut LogSink,
116    ) -> PluginResult<LogRead> {
117        Err(crate::error::PluginError::NotSupported(
118            "Run logs not supported by this provider".to_string(),
119        ))
120    }
121
122    async fn fetch_available_pipelines_filtered(
123        &self, org: Option<String>, search: Option<String>,
124        params: Option<crate::types::PaginationParams>,
125    ) -> PluginResult<crate::types::PaginatedAvailablePipelines> {
126        let response = self.fetch_available_pipelines(params.clone()).await?;
127
128        let mut filtered_items = response.items;
129
130        if let Some(org_filter) = org {
131            filtered_items.retain(|p| p.organization.as_ref() == Some(&org_filter));
132        }
133
134        if let Some(search_term) = search {
135            let search_lower = search_term.to_lowercase();
136            filtered_items.retain(|p| {
137                p.name.to_lowercase().contains(&search_lower)
138                    || p.id.to_lowercase().contains(&search_lower)
139                    || p.description
140                        .as_ref()
141                        .is_some_and(|d| d.to_lowercase().contains(&search_lower))
142            });
143        }
144
145        let total_count = filtered_items.len();
146        let params = params.unwrap_or_default();
147
148        Ok(crate::types::PaginatedResponse::new(
149            filtered_items,
150            params.page,
151            params.page_size,
152            total_count,
153        ))
154    }
155
156    async fn fetch_pipelines(&self) -> PluginResult<Vec<Pipeline>>;
157
158    async fn fetch_pipelines_paginated(
159        &self, page: usize, page_size: usize,
160    ) -> PluginResult<crate::types::PaginatedResponse<Pipeline>> {
161        let all_pipelines = self.fetch_pipelines().await?;
162        let total_count = all_pipelines.len();
163        let start = (page - 1) * page_size;
164        let end = start + page_size;
165
166        let pipelines = if start < total_count {
167            all_pipelines[start..end.min(total_count)].to_vec()
168        } else {
169            Vec::new()
170        };
171
172        Ok(crate::types::PaginatedResponse::new(
173            pipelines,
174            page,
175            page_size,
176            total_count,
177        ))
178    }
179
180    async fn fetch_run_history(
181        &self, pipeline_id: &str, limit: usize,
182    ) -> PluginResult<Vec<PipelineRun>>;
183
184    async fn fetch_run_details(
185        &self, pipeline_id: &str, run_number: i64,
186    ) -> PluginResult<PipelineRun>;
187
188    async fn trigger_pipeline(&self, params: TriggerParams) -> PluginResult<String>;
189
190    async fn cancel_run(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
191        Err(crate::error::PluginError::NotSupported(
192            "Run cancellation not supported by this provider".to_string(),
193        ))
194    }
195
196    async fn review_run(
197        &self, _pipeline_id: &str, _run_number: i64, _decision: ReviewDecision,
198    ) -> PluginResult<()> {
199        Err(crate::error::PluginError::NotSupported(
200            "Run review not supported by this provider".to_string(),
201        ))
202    }
203
204    async fn rerun_failed_jobs(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
205        Err(crate::error::PluginError::NotSupported(
206            "Re-running failed jobs not supported by this provider".to_string(),
207        ))
208    }
209
210    async fn rerun_run(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
211        Err(crate::error::PluginError::NotSupported(
212            "Re-running runs not supported by this provider".to_string(),
213        ))
214    }
215
216    async fn fetch_workflow_parameters(
217        &self, _workflow_id: &str,
218    ) -> PluginResult<Vec<WorkflowParameter>> {
219        Ok(Vec::new())
220    }
221
222    async fn fetch_agents(&self) -> PluginResult<Vec<BuildAgent>> {
223        Err(crate::error::PluginError::NotSupported(
224            "Agent monitoring not supported by this provider".to_string(),
225        ))
226    }
227
228    async fn fetch_artifacts(&self, _run_id: &str) -> PluginResult<Vec<BuildArtifact>> {
229        Err(crate::error::PluginError::NotSupported(
230            "Artifact fetching not supported by this provider".to_string(),
231        ))
232    }
233
234    async fn fetch_queues(&self) -> PluginResult<Vec<BuildQueue>> {
235        Err(crate::error::PluginError::NotSupported(
236            "Queue monitoring not supported by this provider".to_string(),
237        ))
238    }
239
240    async fn fetch_rate_limit(&self) -> PluginResult<Option<RateLimit>> {
241        Ok(None)
242    }
243
244    fn get_migrations(&self) -> Vec<String> {
245        Vec::new()
246    }
247
248    fn provider_type(&self) -> &str {
249        &self.metadata().provider_type
250    }
251
252    async fn get_field_options(
253        &self, _field_key: &str, _config: &HashMap<String, String>,
254    ) -> PluginResult<Vec<String>> {
255        Ok(Vec::new())
256    }
257
258    async fn sign_in_client(
259        &self, _config: &HashMap<String, String>, _http_client: &reqwest::Client,
260    ) -> PluginResult<SignInClient> {
261        Err(crate::error::PluginError::NotSupported(
262            "Sign-in is not supported by this provider".to_string(),
263        ))
264    }
265
266    async fn check_permissions(&self) -> PluginResult<PermissionStatus> {
267        let required_permissions = &self.metadata().required_permissions;
268        let permissions = required_permissions
269            .iter()
270            .map(|p| PermissionCheck {
271                permission: p.clone(),
272                granted: true,
273            })
274            .collect();
275
276        Ok(PermissionStatus {
277            permissions,
278            all_granted: true,
279            checked_at: chrono::Utc::now(),
280            metadata: std::collections::HashMap::new(),
281        })
282    }
283
284    fn get_feature_availability(&self, status: &PermissionStatus) -> Vec<FeatureAvailability> {
285        let features = &self.metadata().features;
286        let granted_perms: std::collections::HashSet<String> = status
287            .permissions
288            .iter()
289            .filter(|p| p.granted)
290            .map(|p| p.permission.name.clone())
291            .collect();
292
293        features
294            .iter()
295            .map(|feature| {
296                let missing: Vec<String> = feature
297                    .required_permissions
298                    .iter()
299                    .filter(|p| !granted_perms.contains(*p))
300                    .cloned()
301                    .collect();
302
303                FeatureAvailability {
304                    feature: feature.clone(),
305                    available: missing.is_empty(),
306                    missing_permissions: missing,
307                }
308            })
309            .collect()
310    }
311}