config.rs 5.6 KB

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