config.rs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. use crate::github::GithubClient;
  2. use std::collections::{HashMap, HashSet};
  3. use std::fmt;
  4. use std::sync::{Arc, RwLock};
  5. use std::time::{Duration, Instant};
  6. static CONFIG_FILE_NAME: &str = "triagebot.toml";
  7. const REFRESH_EVERY: Duration = Duration::from_secs(2 * 60); // Every two minutes
  8. lazy_static::lazy_static! {
  9. static ref CONFIG_CACHE:
  10. RwLock<HashMap<String, (Result<Arc<Config>, ConfigurationError>, Instant)>> =
  11. RwLock::new(HashMap::new());
  12. }
  13. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  14. #[serde(rename_all = "kebab-case")]
  15. pub(crate) struct Config {
  16. pub(crate) relabel: Option<RelabelConfig>,
  17. pub(crate) assign: Option<AssignConfig>,
  18. pub(crate) ping: Option<PingConfig>,
  19. pub(crate) nominate: Option<NominateConfig>,
  20. pub(crate) prioritize: Option<PrioritizeConfig>,
  21. pub(crate) major_change: Option<MajorChangeConfig>,
  22. pub(crate) glacier: Option<GlacierConfig>,
  23. }
  24. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  25. pub(crate) struct NominateConfig {
  26. // team name -> label
  27. pub(crate) teams: HashMap<String, String>,
  28. }
  29. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  30. pub(crate) struct PingConfig {
  31. // team name -> message
  32. // message will have the cc string appended
  33. #[serde(flatten)]
  34. teams: HashMap<String, PingTeamConfig>,
  35. }
  36. impl PingConfig {
  37. pub(crate) fn get_by_name(&self, team: &str) -> Option<(&str, &PingTeamConfig)> {
  38. if let Some((team, cfg)) = self.teams.get_key_value(team) {
  39. return Some((team, cfg));
  40. }
  41. for (name, cfg) in self.teams.iter() {
  42. if cfg.alias.contains(team) {
  43. return Some((name, cfg));
  44. }
  45. }
  46. None
  47. }
  48. }
  49. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  50. pub(crate) struct PingTeamConfig {
  51. pub(crate) message: String,
  52. #[serde(default)]
  53. pub(crate) alias: HashSet<String>,
  54. pub(crate) label: Option<String>,
  55. }
  56. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  57. pub(crate) struct AssignConfig {
  58. #[serde(default)]
  59. _empty: (),
  60. }
  61. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  62. #[serde(rename_all = "kebab-case")]
  63. pub(crate) struct RelabelConfig {
  64. #[serde(default)]
  65. pub(crate) allow_unauthenticated: Vec<String>,
  66. }
  67. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  68. pub(crate) struct PrioritizeConfig {
  69. pub(crate) label: String,
  70. #[serde(default)]
  71. pub(crate) prioritize_on: Vec<String>,
  72. #[serde(default)]
  73. pub(crate) exclude_labels: Vec<String>,
  74. pub(crate) zulip_stream: u64,
  75. }
  76. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  77. pub(crate) struct MajorChangeConfig {
  78. pub(crate) second_label: String,
  79. pub(crate) meeting_label: String,
  80. pub(crate) zulip_stream: u64,
  81. }
  82. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  83. pub(crate) struct GlacierConfig {}
  84. pub(crate) async fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, ConfigurationError> {
  85. if let Some(config) = get_cached_config(repo) {
  86. log::trace!("returning config for {} from cache", repo);
  87. config
  88. } else {
  89. log::trace!("fetching fresh config for {}", repo);
  90. let res = get_fresh_config(gh, repo).await;
  91. CONFIG_CACHE
  92. .write()
  93. .unwrap()
  94. .insert(repo.to_string(), (res.clone(), Instant::now()));
  95. res
  96. }
  97. }
  98. fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
  99. let cache = CONFIG_CACHE.read().unwrap();
  100. cache.get(repo).and_then(|(config, fetch_time)| {
  101. if fetch_time.elapsed() < REFRESH_EVERY {
  102. Some(config.clone())
  103. } else {
  104. None
  105. }
  106. })
  107. }
  108. async fn get_fresh_config(
  109. gh: &GithubClient,
  110. repo: &str,
  111. ) -> Result<Arc<Config>, ConfigurationError> {
  112. let contents = gh
  113. .raw_file(repo, "master", CONFIG_FILE_NAME)
  114. .await
  115. .map_err(|e| ConfigurationError::Http(Arc::new(e)))?
  116. .ok_or(ConfigurationError::Missing)?;
  117. let config = Arc::new(toml::from_slice::<Config>(&contents).map_err(ConfigurationError::Toml)?);
  118. log::debug!("fresh configuration for {}: {:?}", repo, config);
  119. Ok(config)
  120. }
  121. #[derive(Clone, Debug)]
  122. pub enum ConfigurationError {
  123. Missing,
  124. Toml(toml::de::Error),
  125. Http(Arc<anyhow::Error>),
  126. }
  127. impl std::error::Error for ConfigurationError {}
  128. impl fmt::Display for ConfigurationError {
  129. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  130. match self {
  131. ConfigurationError::Missing => write!(
  132. f,
  133. "This repository is not enabled to use triagebot.\n\
  134. Add a `triagebot.toml` in the root of the master branch to enable it."
  135. ),
  136. ConfigurationError::Toml(e) => {
  137. write!(f, "Malformed `triagebot.toml` in master branch.\n{}", e)
  138. }
  139. ConfigurationError::Http(_) => {
  140. write!(f, "Failed to query configuration for this repository.")
  141. }
  142. }
  143. }
  144. }
  145. #[cfg(test)]
  146. mod tests {
  147. use super::*;
  148. #[test]
  149. fn sample() {
  150. let config = r#"
  151. [relabel]
  152. allow-unauthenticated = [
  153. "C-*"
  154. ]
  155. [assign]
  156. [ping.compiler]
  157. message = """\
  158. So many people!\
  159. """
  160. label = "T-compiler"
  161. [ping.wg-meta]
  162. message = """\
  163. Testing\
  164. """
  165. [nominate.teams]
  166. compiler = "T-compiler"
  167. release = "T-release"
  168. core = "T-core"
  169. infra = "T-infra"
  170. "#;
  171. let config = toml::from_str::<Config>(&config).unwrap();
  172. let mut ping_teams = HashMap::new();
  173. ping_teams.insert(
  174. "compiler".to_owned(),
  175. PingTeamConfig {
  176. message: "So many people!".to_owned(),
  177. label: Some("T-compiler".to_owned()),
  178. alias: HashSet::new(),
  179. },
  180. );
  181. ping_teams.insert(
  182. "wg-meta".to_owned(),
  183. PingTeamConfig {
  184. message: "Testing".to_owned(),
  185. label: None,
  186. alias: HashSet::new(),
  187. },
  188. );
  189. let mut nominate_teams = HashMap::new();
  190. nominate_teams.insert("compiler".to_owned(), "T-compiler".to_owned());
  191. nominate_teams.insert("release".to_owned(), "T-release".to_owned());
  192. nominate_teams.insert("core".to_owned(), "T-core".to_owned());
  193. nominate_teams.insert("infra".to_owned(), "T-infra".to_owned());
  194. assert_eq!(
  195. config,
  196. Config {
  197. relabel: Some(RelabelConfig {
  198. allow_unauthenticated: vec!["C-*".into()],
  199. }),
  200. assign: Some(AssignConfig { _empty: () }),
  201. ping: Some(PingConfig { teams: ping_teams }),
  202. nominate: Some(NominateConfig {
  203. teams: nominate_teams
  204. }),
  205. prioritize: None,
  206. major_change: None,
  207. glacier: None,
  208. }
  209. );
  210. }
  211. }