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}