From 77ad60dd0661b1370cf33de2ff40e4da6d4db713 Mon Sep 17 00:00:00 2001 From: Louis Vallat Date: Thu, 2 Jun 2022 17:54:58 +0200 Subject: [PATCH] Initial commit Signed-off-by: Louis Vallat --- .gitignore | 3 + Cargo.toml | 15 +++++ src/config_manager.rs | 36 +++++++++++ src/gitlab_controller.rs | 115 ++++++++++++++++++++++++++++++++++++ src/launchpad_controller.rs | 56 ++++++++++++++++++ src/main.rs | 33 +++++++++++ 6 files changed, 258 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/config_manager.rs create mode 100644 src/gitlab_controller.rs create mode 100644 src/launchpad_controller.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47929f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +config.json +*.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a027b78 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "gitlab-rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +gitlab = "0.1500" +serde = "1.0" +serde_json = "1.0" +log = "0.4.17" +env_logger = "0.9.0" +launchy = "0.2.0" +open = "2.1.2" diff --git a/src/config_manager.rs b/src/config_manager.rs new file mode 100644 index 0000000..a243c54 --- /dev/null +++ b/src/config_manager.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +use std::path::Path; + +const CONFIG_FILENAME: &str = "./config.json"; + +// TODO REMOVE PUB AND REPLACE BY FN +#[derive(Clone, Deserialize, Serialize)] +pub struct Config { + pub host: String, + pub key: String, + pub projects: Vec +} + +// TODO REMOVE PUB AND REPLACE BY FN +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Project { + pub name: String, + pub ref_: String, + pub abs_x: u8, + pub abs_y: u8 +} + +pub fn get_config() -> Option { + if Path::exists(Path::new(CONFIG_FILENAME)) { + return serde_json::from_str( + std::fs::read_to_string(CONFIG_FILENAME).unwrap().as_str()) + .unwrap(); + } + return None; +} + +pub fn save_config(config: &Config) { + std::fs::write(Path::new(CONFIG_FILENAME), + serde_json::to_string_pretty(config).unwrap()).unwrap(); +} diff --git a/src/gitlab_controller.rs b/src/gitlab_controller.rs new file mode 100644 index 0000000..c879f31 --- /dev/null +++ b/src/gitlab_controller.rs @@ -0,0 +1,115 @@ +use std::{thread::{sleep, self, JoinHandle}, time::Duration}; + +use launchy::{launchpad_mini::{Button, Color, Output, DoubleBufferingBehavior}, OutputDevice}; +use serde::Deserialize; +use log::info; +use gitlab::{Gitlab, api::{projects::pipelines::{Pipelines, PipelineOrderBy}, Query}}; + +use crate::config_manager::{Config, Project}; + +const REFRESH_DELAY: Duration = Duration::from_millis(2000); + +#[derive(Debug, Clone, Deserialize)] +pub struct Pipeline { + id: u64, + status: String +} + +enum PipelineStatus { + Created, + WaitingForResource, + Preparing, + Pending, + Running, + Success, + Failed, + Canceled, + Skipped, + Manual, + Scheduled +} + +impl PipelineStatus { + fn from(s: &str) -> Option { + return match s { + "created" => Some(PipelineStatus::Created), + "waiting_for_resource" => Some(PipelineStatus::WaitingForResource), + "preparing" => Some(PipelineStatus::Preparing), + "pending" => Some(PipelineStatus::Pending), + "running" => Some(PipelineStatus::Running), + "success" => Some(PipelineStatus::Success), + "failed" => Some(PipelineStatus::Failed), + "canceled" => Some(PipelineStatus::Canceled), + "skipped" => Some(PipelineStatus::Skipped), + "manual" => Some(PipelineStatus::Manual), + "scheduled" => Some(PipelineStatus::Scheduled), + _ => None + } + } + + fn get_color(&self) -> (Color, bool) { + return match self { + PipelineStatus::Pending => (Color::YELLOW, false), + PipelineStatus::Running => (Color::AMBER, true), + PipelineStatus::Success => (Color::GREEN, false), + _ => (Color::OFF, false) + }; + } +} + + +pub fn refresh_on_timer(client: Gitlab, project: Project) { + let mut output = Output::guess().unwrap(); + loop { + let pipeline = get_latest_pipelines(&client, &project); + if pipeline.is_none() { return; } + let c = PipelineStatus::from(pipeline.unwrap().status.as_str()) + .unwrap(); + output.set_button(Button::GridButton { x: project.abs_x, y: project.abs_y }, + c.get_color().0, + if c.get_color().1 { DoubleBufferingBehavior::None } + else { DoubleBufferingBehavior::Copy }).unwrap(); + sleep(REFRESH_DELAY); + } +} + +pub fn load_from_config(config: Config) -> Vec> { + info!("Loading gitlab manager from configuration."); + let mut threads = vec![]; + for p in config.projects.iter() { + let client = Gitlab::new(config.host.as_str(), config.key.as_str()).unwrap(); + let project = p.clone(); + threads.push(thread::spawn(move|| refresh_on_timer(client, project))); + } + return threads; +} + +pub fn get_latest_pipelines(client: &Gitlab, project: &Project) -> Option { + let endpoint = Pipelines::builder() + .project(project.name.as_str()).ref_(project.ref_.as_str()) + .order_by(PipelineOrderBy::UpdatedAt).build().unwrap(); + let pipelines: Vec = endpoint.query(client).unwrap_or(vec![]); + return if pipelines.is_empty() { None } else { + Some(pipelines.first().unwrap().to_owned()) }; +} + +pub fn retry_pipeline(host: &str, token: &str, project: &Project) { + let client = Gitlab::new(host, token).unwrap(); + let pipeline = get_latest_pipelines(&client, &project); + if pipeline.is_none() { return; } + let _res: Pipeline; + if pipeline.clone().unwrap().status == "success" { + let endpoint = gitlab::api::projects::pipelines::CreatePipeline::builder() + .project(project.name.clone()) + .ref_(project.ref_.clone()) + .build().unwrap(); + _res = endpoint.query(&client).unwrap(); + } else { + let endpoint = gitlab::api::projects::pipelines::RetryPipeline::builder() + .project(project.name.clone()) + .pipeline(pipeline.unwrap().id) + .build().unwrap(); + _res = endpoint.query(&client).unwrap(); + } +} + diff --git a/src/launchpad_controller.rs b/src/launchpad_controller.rs new file mode 100644 index 0000000..1533537 --- /dev/null +++ b/src/launchpad_controller.rs @@ -0,0 +1,56 @@ +use launchy::{OutputDevice, InputDevice, launchpad_mini::{Output, Input, Buffer, Button, Color, DoubleBuffering, DoubleBufferingBehavior, Message}, MsgPollingWrapper}; + +use crate::{config_manager::Config, gitlab_controller::retry_pipeline}; + +const RESTART_BUTTON: Button = Button::GridButton { x: 8, y: 0 }; + +fn engage_restart(output: &mut Output) { + output.set_button(RESTART_BUTTON, Color::DIM_GREEN, DoubleBufferingBehavior::Copy).unwrap(); +} + +fn disengage_restart(output: &mut Output) { + output.set_button(RESTART_BUTTON, Color::OFF, DoubleBufferingBehavior::Copy).unwrap(); +} + +fn running_thread(config: Config) { + let mut output = Output::guess().unwrap(); + let input = Input::guess_polling().unwrap(); + let mut restart = false; + for msg in input.iter() { + if let Message::Release { button } = msg { + if button == RESTART_BUTTON { + if !restart { + restart = true; + engage_restart(&mut output); + } else { + restart = false; + disengage_restart(&mut output); + } + } else if button.abs_x() != 8 { + let project = config.projects.iter() + .find(|p| p.abs_x == button.abs_x() && p.abs_y + 1 == button.abs_y()); + if project.is_none() { continue; } + let project = project.unwrap(); + if restart { + restart = false; + retry_pipeline(config.host.as_str(), config.key.as_str(), project); + disengage_restart(&mut output); + } else { + open::that_in_background(format!("https://{}/{}", config.host, project.name)); + } + } + } + } +} + +pub fn init(config: Config) { + let mut output = launchy::launchpad_mini::Output::guess().unwrap(); + output.reset().unwrap(); + output.control_double_buffering(DoubleBuffering { + copy: false, + flash: true, + edited_buffer: Buffer::A, + displayed_buffer: Buffer::A + }).unwrap(); + std::thread::spawn(move || running_thread(config)); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6a18159 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,33 @@ +use log::error; + +use crate::config_manager::Config; +use crate::config_manager::Project; + +mod config_manager; +mod launchpad_controller; +mod gitlab_controller; + +fn main() { + env_logger::init(); + let conf = config_manager::get_config(); + if conf.is_none() { + config_manager::save_config(&Config { + host: "gitlab-host".to_string(), + key: "your-gitlab-key".to_string(), + projects: vec![Project { + name: "example".to_string(), + ref_: "master".to_string(), + abs_x: 0, + abs_y: 0 + }] + }); + error!("Please configure the application with the configuration file."); + return; + } + let conf = conf.unwrap(); + launchpad_controller::init(conf.clone()); + let threads = gitlab_controller::load_from_config(conf); + for t in threads { + t.join().unwrap(); + } +}