config.rs 5.6 KB

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