pipedash_plugin_buildkite/
plugin.rs1use std::collections::{
2 BTreeSet,
3 HashMap,
4};
5use std::time::Duration;
6
7use async_trait::async_trait;
8use futures::future::join_all;
9use pipedash_plugin_api::*;
10
11use crate::{
12 client,
13 config,
14 mapper,
15 metadata,
16};
17
18pub struct BuildkitePlugin {
19 metadata: PluginMetadata,
20 client: Option<client::BuildkiteClient>,
21 provider_id: Option<i64>,
22 config: HashMap<String, String>,
23}
24
25impl Default for BuildkitePlugin {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl BuildkitePlugin {
32 pub fn new() -> Self {
33 Self {
34 metadata: metadata::create_metadata(),
35 client: None,
36 provider_id: None,
37 config: HashMap::new(),
38 }
39 }
40
41 fn client(&self) -> PluginResult<&client::BuildkiteClient> {
42 self.client
43 .as_ref()
44 .ok_or_else(|| PluginError::Internal("Plugin not initialized".to_string()))
45 }
46}
47
48#[async_trait]
49impl Plugin for BuildkitePlugin {
50 fn metadata(&self) -> &PluginMetadata {
51 &self.metadata
52 }
53
54 fn initialize(
55 &mut self, provider_id: i64, config: HashMap<String, String>,
56 http_client: Option<std::sync::Arc<reqwest::Client>>,
57 ) -> PluginResult<()> {
58 let token = config
59 .get("token")
60 .ok_or_else(|| PluginError::InvalidConfig("Missing Buildkite API token".to_string()))?
61 .to_string();
62
63 let client = http_client.unwrap_or_else(|| {
64 std::sync::Arc::new(
65 reqwest::Client::builder()
66 .use_rustls_tls()
67 .pool_max_idle_per_host(10)
68 .timeout(Duration::from_secs(30))
69 .connect_timeout(Duration::from_secs(10))
70 .tcp_keepalive(Duration::from_secs(60))
71 .build()
72 .expect("Failed to build HTTP client"),
73 )
74 });
75
76 self.client = Some(client::BuildkiteClient::new(client, token));
77 self.provider_id = Some(provider_id);
78 self.config = config;
79
80 Ok(())
81 }
82
83 async fn validate_credentials(&self) -> PluginResult<bool> {
84 let client = self.client()?;
85
86 client.fetch_organizations().await?;
87
88 Ok(true)
89 }
90
91 async fn fetch_available_pipelines(
92 &self, params: Option<PaginationParams>,
93 ) -> PluginResult<PaginatedResponse<AvailablePipeline>> {
94 let client = self.client()?;
95 client::fetch_all_available_pipelines(client, params).await
96 }
97
98 async fn fetch_organizations(&self) -> PluginResult<Vec<pipedash_plugin_api::Organization>> {
99 let client = self.client()?;
100 let orgs = client.fetch_organizations().await?;
101
102 Ok(orgs
103 .into_iter()
104 .map(|org| pipedash_plugin_api::Organization {
105 id: org.slug.clone(),
106 name: org.name,
107 description: org.description,
108 })
109 .collect())
110 }
111
112 async fn fetch_available_pipelines_filtered(
113 &self, org: Option<String>, search: Option<String>, params: Option<PaginationParams>,
114 ) -> PluginResult<PaginatedResponse<AvailablePipeline>> {
115 let client = self.client()?;
116 client::fetch_available_pipelines_filtered(client, org, search, params).await
117 }
118
119 async fn fetch_pipelines(&self) -> PluginResult<Vec<Pipeline>> {
120 let provider_id = self
121 .provider_id
122 .ok_or_else(|| PluginError::Internal("Provider ID not set".to_string()))?;
123
124 let pipelines = config::parse_selected_items(&self.config)?;
125
126 if pipelines.is_empty() {
127 return Err(PluginError::InvalidConfig(
128 "No pipelines configured".to_string(),
129 ));
130 }
131
132 let client = self.client()?;
133 let futures = pipelines
134 .into_iter()
135 .map(|(org, slug)| client.fetch_pipeline(provider_id, org, slug));
136
137 let results = join_all(futures).await;
138
139 let mut all_pipelines = Vec::new();
140 let mut errors = Vec::new();
141
142 for result in results {
143 match result {
144 Ok(pipeline) => all_pipelines.push(pipeline),
145 Err(e) => errors.push(e),
146 }
147 }
148
149 if !errors.is_empty() && all_pipelines.is_empty() {
150 return Err(errors.into_iter().next().unwrap());
151 }
152
153 Ok(all_pipelines)
154 }
155
156 async fn fetch_run_history(
157 &self, pipeline_id: &str, limit: usize,
158 ) -> PluginResult<Vec<PipelineRun>> {
159 let parts: Vec<&str> = pipeline_id.split("__").collect();
160 if parts.len() != 4 {
161 return Err(PluginError::InvalidConfig(format!(
162 "Invalid pipeline ID format: {pipeline_id}"
163 )));
164 }
165
166 let org = parts[2];
167 let slug = parts[3];
168
169 let client = self.client()?;
170 let builds = client.fetch_builds(org, slug, limit).await?;
171
172 let pipeline_runs = builds
173 .into_iter()
174 .map(|build| client::build_to_pipeline_run(build, pipeline_id))
175 .collect();
176
177 Ok(pipeline_runs)
178 }
179
180 async fn fetch_run_details(
181 &self, pipeline_id: &str, run_number: i64,
182 ) -> PluginResult<PipelineRun> {
183 let parts: Vec<&str> = pipeline_id.split("__").collect();
184 if parts.len() != 4 {
185 return Err(PluginError::InvalidConfig(format!(
186 "Invalid pipeline ID format: {pipeline_id}"
187 )));
188 }
189
190 let org = parts[2];
191 let slug = parts[3];
192
193 let client = self.client()?;
194 let builds = client.fetch_builds(org, slug, 100).await?;
195
196 let build = builds
197 .into_iter()
198 .find(|b| b.number == run_number)
199 .ok_or_else(|| {
200 PluginError::PipelineNotFound(format!(
201 "Build #{run_number} not found for pipeline {pipeline_id}"
202 ))
203 })?;
204
205 Ok(client::build_to_pipeline_run(build, pipeline_id))
206 }
207
208 async fn fetch_workflow_parameters(
209 &self, _workflow_id: &str,
210 ) -> PluginResult<Vec<WorkflowParameter>> {
211 Ok(vec![WorkflowParameter {
212 name: "branch".to_string(),
213 label: Some("Branch".to_string()),
214 description: Some("Branch to build".to_string()),
215 param_type: WorkflowParameterType::String {
216 default: Some("main".to_string()),
217 },
218 required: true,
219 }])
220 }
221
222 async fn trigger_pipeline(&self, params: TriggerParams) -> PluginResult<String> {
223 let parts: Vec<&str> = params.workflow_id.split("__").collect();
224 if parts.len() != 4 {
225 return Err(PluginError::InvalidConfig(format!(
226 "Invalid workflow ID format: {}",
227 params.workflow_id
228 )));
229 }
230
231 let org = parts[2];
232 let slug = parts[3];
233
234 let branch = params
235 .inputs
236 .as_ref()
237 .and_then(|inputs| inputs.get("branch"))
238 .and_then(|v| v.as_str())
239 .unwrap_or("main")
240 .to_string();
241
242 let client = self.client()?;
243 let build = client
244 .trigger_build(org, slug, branch.clone(), params.inputs)
245 .await?;
246
247 Ok(serde_json::json!({
248 "message": format!("Triggered build #{} on branch {}", build.number, branch),
249 "build_number": build.number,
250 "build_url": build.web_url
251 })
252 .to_string())
253 }
254
255 async fn fetch_agents(&self) -> PluginResult<Vec<BuildAgent>> {
256 let orgs: BTreeSet<String> = config::parse_selected_items(&self.config)?
257 .into_iter()
258 .map(|(org, _)| org)
259 .collect();
260
261 let client = self.client()?;
262 let mut agents = Vec::new();
263 for org in orgs {
264 agents.extend(client.fetch_agents(&org).await?);
265 }
266
267 Ok(agents.into_iter().map(mapper::map_agent).collect())
268 }
269
270 async fn fetch_artifacts(&self, run_id: &str) -> PluginResult<Vec<BuildArtifact>> {
271 let parts: Vec<&str> = run_id.split("__").collect();
272 let [_, _, org, _, build_id] = parts.as_slice() else {
273 return Err(PluginError::InvalidConfig(format!(
274 "Invalid run ID: {run_id}"
275 )));
276 };
277
278 let client = self.client()?;
279 let artifacts = client.fetch_artifacts(org, build_id).await?;
280
281 Ok(artifacts
282 .into_iter()
283 .map(|artifact| client::artifact_to_build_artifact(artifact, run_id))
284 .collect())
285 }
286
287 async fn cancel_run(&self, pipeline_id: &str, run_number: i64) -> PluginResult<()> {
288 let parts: Vec<&str> = pipeline_id.split("__").collect();
289 if parts.len() != 4 {
290 return Err(PluginError::InvalidConfig(format!(
291 "Invalid pipeline ID format: {pipeline_id}"
292 )));
293 }
294
295 let org = parts[2];
296 let slug = parts[3];
297
298 let client = self.client()?;
299 client.cancel_build(org, slug, run_number).await
300 }
301}