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::sign_in::SignInClient;
27use crate::types::*;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct PluginMetadata {
31    pub name: String,
32    pub provider_type: String,
33    pub version: String,
34    pub description: String,
35    pub author: Option<String>,
36    pub icon: Option<String>,
37    pub config_schema: ConfigSchema,
38    pub table_schema: TableSchema,
39    pub capabilities: PluginCapabilities,
40    #[serde(default)]
41    pub required_permissions: Vec<Permission>,
42    #[serde(default)]
43    pub features: Vec<Feature>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, Default)]
47pub struct PluginCapabilities {
48    pub pipelines: bool,
49    pub pipeline_runs: bool,
50    pub trigger: bool,
51    pub cancel: bool,
52    pub agents: bool,
53    pub artifacts: bool,
54    pub queues: bool,
55    pub custom_tables: bool,
56    #[serde(default)]
57    pub retains_run_history: bool,
58    #[serde(default)]
59    pub logs: bool,
60    #[serde(default)]
61    pub sign_in: bool,
62}
63
64#[async_trait]
65pub trait Plugin: Send + Sync {
66    fn metadata(&self) -> &PluginMetadata;
67
68    fn initialize(
69        &mut self, provider_id: i64, config: HashMap<String, String>,
70        http_client: Option<std::sync::Arc<reqwest::Client>>,
71    ) -> PluginResult<()>;
72
73    fn client_options(&self, _config: &HashMap<String, String>) -> ClientOptions {
74        ClientOptions::default()
75    }
76
77    fn status_source(&self) -> Option<&StatusSource> {
78        None
79    }
80
81    async fn check_health(&self) -> PluginResult<Vec<ComponentHealth>> {
82        Ok(Vec::new())
83    }
84
85    async fn validate_credentials(&self) -> PluginResult<bool>;
86
87    async fn fetch_available_pipelines(
88        &self, params: Option<crate::types::PaginationParams>,
89    ) -> PluginResult<crate::types::PaginatedAvailablePipelines> {
90        let _ = params;
91        Ok(crate::types::PaginatedResponse::empty())
92    }
93
94    async fn fetch_organizations(&self) -> PluginResult<Vec<crate::types::Organization>> {
95        Ok(Vec::new())
96    }
97
98    async fn ids_with_ci(&self, ids: Vec<String>) -> PluginResult<Vec<String>> {
99        Ok(ids)
100    }
101
102    fn log_dialect(&self) -> &'static dyn LogDialect {
103        &PlainDialect
104    }
105
106    async fn fetch_log_nodes(&self, _run: RunRef<'_>) -> PluginResult<Vec<LogNode>> {
107        Err(crate::error::PluginError::NotSupported(
108            "Run logs not supported by this provider".to_string(),
109        ))
110    }
111
112    async fn read_log(
113        &self, _run: RunRef<'_>, _node_id: &str, _sink: &mut LogSink,
114    ) -> PluginResult<()> {
115        Err(crate::error::PluginError::NotSupported(
116            "Run logs not supported by this provider".to_string(),
117        ))
118    }
119
120    async fn fetch_available_pipelines_filtered(
121        &self, org: Option<String>, search: Option<String>,
122        params: Option<crate::types::PaginationParams>,
123    ) -> PluginResult<crate::types::PaginatedAvailablePipelines> {
124        let response = self.fetch_available_pipelines(params.clone()).await?;
125
126        let mut filtered_items = response.items;
127
128        if let Some(org_filter) = org {
129            filtered_items.retain(|p| p.organization.as_ref() == Some(&org_filter));
130        }
131
132        if let Some(search_term) = search {
133            let search_lower = search_term.to_lowercase();
134            filtered_items.retain(|p| {
135                p.name.to_lowercase().contains(&search_lower)
136                    || p.id.to_lowercase().contains(&search_lower)
137                    || p.description
138                        .as_ref()
139                        .is_some_and(|d| d.to_lowercase().contains(&search_lower))
140            });
141        }
142
143        let total_count = filtered_items.len();
144        let params = params.unwrap_or_default();
145
146        Ok(crate::types::PaginatedResponse::new(
147            filtered_items,
148            params.page,
149            params.page_size,
150            total_count,
151        ))
152    }
153
154    async fn fetch_pipelines(&self) -> PluginResult<Vec<Pipeline>>;
155
156    async fn fetch_pipelines_paginated(
157        &self, page: usize, page_size: usize,
158    ) -> PluginResult<crate::types::PaginatedResponse<Pipeline>> {
159        let all_pipelines = self.fetch_pipelines().await?;
160        let total_count = all_pipelines.len();
161        let start = (page - 1) * page_size;
162        let end = start + page_size;
163
164        let pipelines = if start < total_count {
165            all_pipelines[start..end.min(total_count)].to_vec()
166        } else {
167            Vec::new()
168        };
169
170        Ok(crate::types::PaginatedResponse::new(
171            pipelines,
172            page,
173            page_size,
174            total_count,
175        ))
176    }
177
178    async fn fetch_run_history(
179        &self, pipeline_id: &str, limit: usize,
180    ) -> PluginResult<Vec<PipelineRun>>;
181
182    async fn fetch_run_details(
183        &self, pipeline_id: &str, run_number: i64,
184    ) -> PluginResult<PipelineRun>;
185
186    async fn trigger_pipeline(&self, params: TriggerParams) -> PluginResult<String>;
187
188    async fn cancel_run(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
189        Err(crate::error::PluginError::NotSupported(
190            "Run cancellation not supported by this provider".to_string(),
191        ))
192    }
193
194    async fn review_run(
195        &self, _pipeline_id: &str, _run_number: i64, _decision: ReviewDecision,
196    ) -> PluginResult<()> {
197        Err(crate::error::PluginError::NotSupported(
198            "Run review not supported by this provider".to_string(),
199        ))
200    }
201
202    async fn rerun_failed_jobs(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
203        Err(crate::error::PluginError::NotSupported(
204            "Re-running failed jobs not supported by this provider".to_string(),
205        ))
206    }
207
208    async fn rerun_run(&self, _pipeline_id: &str, _run_number: i64) -> PluginResult<()> {
209        Err(crate::error::PluginError::NotSupported(
210            "Re-running runs not supported by this provider".to_string(),
211        ))
212    }
213
214    async fn fetch_workflow_parameters(
215        &self, _workflow_id: &str,
216    ) -> PluginResult<Vec<WorkflowParameter>> {
217        Ok(Vec::new())
218    }
219
220    async fn fetch_agents(&self) -> PluginResult<Vec<BuildAgent>> {
221        Err(crate::error::PluginError::NotSupported(
222            "Agent monitoring not supported by this provider".to_string(),
223        ))
224    }
225
226    async fn fetch_artifacts(&self, _run_id: &str) -> PluginResult<Vec<BuildArtifact>> {
227        Err(crate::error::PluginError::NotSupported(
228            "Artifact fetching not supported by this provider".to_string(),
229        ))
230    }
231
232    async fn fetch_queues(&self) -> PluginResult<Vec<BuildQueue>> {
233        Err(crate::error::PluginError::NotSupported(
234            "Queue monitoring not supported by this provider".to_string(),
235        ))
236    }
237
238    async fn fetch_rate_limit(&self) -> PluginResult<Option<RateLimit>> {
239        Ok(None)
240    }
241
242    fn get_migrations(&self) -> Vec<String> {
243        Vec::new()
244    }
245
246    fn provider_type(&self) -> &str {
247        &self.metadata().provider_type
248    }
249
250    async fn get_field_options(
251        &self, _field_key: &str, _config: &HashMap<String, String>,
252    ) -> PluginResult<Vec<String>> {
253        Ok(Vec::new())
254    }
255
256    async fn sign_in_client(
257        &self, _config: &HashMap<String, String>, _http_client: &reqwest::Client,
258    ) -> PluginResult<SignInClient> {
259        Err(crate::error::PluginError::NotSupported(
260            "Sign-in is not supported by this provider".to_string(),
261        ))
262    }
263
264    async fn check_permissions(&self) -> PluginResult<PermissionStatus> {
265        let required_permissions = &self.metadata().required_permissions;
266        let permissions = required_permissions
267            .iter()
268            .map(|p| PermissionCheck {
269                permission: p.clone(),
270                granted: true,
271            })
272            .collect();
273
274        Ok(PermissionStatus {
275            permissions,
276            all_granted: true,
277            checked_at: chrono::Utc::now(),
278            metadata: std::collections::HashMap::new(),
279        })
280    }
281
282    fn get_feature_availability(&self, status: &PermissionStatus) -> Vec<FeatureAvailability> {
283        let features = &self.metadata().features;
284        let granted_perms: std::collections::HashSet<String> = status
285            .permissions
286            .iter()
287            .filter(|p| p.granted)
288            .map(|p| p.permission.name.clone())
289            .collect();
290
291        features
292            .iter()
293            .map(|feature| {
294                let missing: Vec<String> = feature
295                    .required_permissions
296                    .iter()
297                    .filter(|p| !granted_perms.contains(*p))
298                    .cloned()
299                    .collect();
300
301                FeatureAvailability {
302                    feature: feature.clone(),
303                    available: missing.is_empty(),
304                    missing_permissions: missing,
305                }
306            })
307            .collect()
308    }
309}