treewide: remove GitHub, generalize, introduce Gerrit

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
This commit is contained in:
raito 2024-10-28 20:30:31 +01:00
parent 8cdae352b0
commit 584659e727
13 changed files with 1214 additions and 581 deletions

1087
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,11 +12,7 @@ chrono = "0.4.22"
either = "1.8.0" either = "1.8.0"
fs2 = "0.4.3" fs2 = "0.4.3"
futures-util = "0.3.25" futures-util = "0.3.25"
#hubcaps = "0.6" hyper = { version = "1.5.0", features = [ "client" ], package = "hyper" }
# for Conclusion::Skipped which is in master
hubcaps = { git = "https://github.com/softprops/hubcaps.git", rev = "d60d157b6638760fc725b2e4e4f329a4ec6b901e", default-features = false, features = ["app", "rustls-tls"] }
# hyper = { version = "0.14", features = ["full"] }
hyper = "=0.10.*"
# maybe can be removed when hyper is updated # maybe can be removed when hyper is updated
http = "0.2" http = "0.2"
lapin = "2.1.1" lapin = "2.1.1"
@ -34,3 +30,4 @@ tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["json", "env-filter"] } tracing-subscriber = { version = "0.3.16", features = ["json", "env-filter"] }
uuid = { version = "1.2", features = ["v4"] } uuid = { version = "1.2", features = ["v4"] }
rustls-pemfile = "1.0.2" rustls-pemfile = "1.0.2"
reqwest = "0.12.9"

View file

@ -72,31 +72,3 @@ impl CommitStatus {
) )
} }
} }
#[derive(Debug)]
pub enum CommitStatusError {
ExpiredCreds(hubcaps::Error),
MissingSha(hubcaps::Error),
Error(hubcaps::Error),
}
impl From<hubcaps::Error> for CommitStatusError {
fn from(e: hubcaps::Error) -> CommitStatusError {
use http::status::StatusCode;
use hubcaps::Error;
match &e {
Error::Fault { code, error }
if code == &StatusCode::UNAUTHORIZED && error.message == "Bad credentials" =>
{
CommitStatusError::ExpiredCreds(e)
}
Error::Fault { code, error }
if code == &StatusCode::UNPROCESSABLE_ENTITY
&& error.message.starts_with("No commit found for SHA:") =>
{
CommitStatusError::MissingSha(e)
}
_otherwise => CommitStatusError::Error(e),
}
}
}

View file

@ -1,26 +1,24 @@
use crate::acl; use crate::acl;
use crate::nix::Nix; use crate::nix::Nix;
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::fs::File; use std::fs::File;
use std::io::{BufReader, Read}; use std::io::Read;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use hubcaps::{Credentials, Github, InstallationTokenGenerator, JWTCredentials};
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use tracing::{debug, error, info, warn}; use tracing::error;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Config { pub struct Config
{
pub runner: RunnerConfig, pub runner: RunnerConfig,
pub feedback: FeedbackConfig, pub feedback: FeedbackConfig,
pub checkout: CheckoutConfig, pub checkout: CheckoutConfig,
pub nix: NixConfig, pub nix: NixConfig,
pub rabbitmq: RabbitMqConfig, pub rabbitmq: RabbitMqConfig,
pub github: Option<GithubConfig>, // TODO: reintroduce VCS configuration somehow
pub github_app: Option<GithubAppConfig>,
pub log_storage: Option<LogStorage>, pub log_storage: Option<LogStorage>,
} }
@ -47,17 +45,6 @@ pub struct NixConfig {
pub initial_heap_size: Option<String>, pub initial_heap_size: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GithubConfig {
pub token_file: PathBuf,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GithubAppConfig {
pub app_id: u64,
pub private_key: PathBuf,
}
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LogStorage { pub struct LogStorage {
pub path: String, pub path: String,
@ -117,25 +104,6 @@ impl Config {
acl::Acl::new(repos, trusted_users) acl::Acl::new(repos, trusted_users)
} }
pub fn github(&self) -> Github {
let token = std::fs::read_to_string(self.github.clone().unwrap().token_file)
.expect("Couldn't read from GitHub token file");
Github::new(
"github.com/grahamc/ofborg",
// tls configured hyper client
Credentials::Token(token),
)
.expect("Unable to create a github client instance")
}
pub fn github_app_vendingmachine(&self) -> GithubAppVendingMachine {
GithubAppVendingMachine {
conf: self.github_app.clone().unwrap(),
id_cache: HashMap::new(),
client_cache: HashMap::new(),
}
}
pub fn nix(&self) -> Nix { pub fn nix(&self) -> Nix {
if self.nix.build_timeout_seconds < 1200 { if self.nix.build_timeout_seconds < 1200 {
error!(?self.nix.build_timeout_seconds, "Please set build_timeout_seconds to at least 1200"); error!(?self.nix.build_timeout_seconds, "Please set build_timeout_seconds to at least 1200");
@ -180,68 +148,6 @@ pub fn load(filename: &Path) -> Config {
deserialized deserialized
} }
pub struct GithubAppVendingMachine {
conf: GithubAppConfig,
id_cache: HashMap<(String, String), Option<u64>>,
client_cache: HashMap<u64, Github>,
}
impl GithubAppVendingMachine {
fn useragent(&self) -> &'static str {
"github.com/grahamc/ofborg (app)"
}
fn jwt(&self) -> JWTCredentials {
let private_key_file =
File::open(self.conf.private_key.clone()).expect("Unable to read private_key");
let mut private_key_reader = BufReader::new(private_key_file);
let private_keys = rustls_pemfile::rsa_private_keys(&mut private_key_reader)
.expect("Unable to convert private_key to DER format");
// We can be reasonably certain that there will only be one private key in this file
let private_key = &private_keys[0];
JWTCredentials::new(self.conf.app_id, private_key.to_vec())
.expect("Unable to create JWTCredentials")
}
fn install_id_for_repo(&mut self, owner: &str, repo: &str) -> Option<u64> {
let useragent = self.useragent();
let jwt = self.jwt();
let key = (owner.to_owned(), repo.to_owned());
*self.id_cache.entry(key).or_insert_with(|| {
info!("Looking up install ID for {}/{}", owner, repo);
let lookup_gh = Github::new(useragent, Credentials::JWT(jwt)).unwrap();
match async_std::task::block_on(lookup_gh.app().find_repo_installation(owner, repo)) {
Ok(install_id) => {
debug!("Received install ID {:?}", install_id);
Some(install_id.id)
}
Err(e) => {
warn!("Error during install ID lookup: {:?}", e);
None
}
}
})
}
pub fn for_repo<'a>(&'a mut self, owner: &str, repo: &str) -> Option<&'a Github> {
let useragent = self.useragent();
let jwt = self.jwt();
let install_id = self.install_id_for_repo(owner, repo)?;
Some(self.client_cache.entry(install_id).or_insert_with(|| {
Github::new(
useragent,
Credentials::InstallationToken(InstallationTokenGenerator::new(install_id, jwt)),
)
.expect("Unable to create a github client instance")
}))
}
}
// Copied from https://stackoverflow.com/a/43627388 // Copied from https://stackoverflow.com/a/43627388
fn deserialize_one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> fn deserialize_one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where where

View file

@ -41,6 +41,7 @@ pub mod systems;
pub mod tagger; pub mod tagger;
pub mod tasks; pub mod tasks;
pub mod test_scratch; pub mod test_scratch;
pub mod vcs;
pub mod worker; pub mod worker;
pub mod writetoline; pub mod writetoline;
@ -66,6 +67,7 @@ pub mod ofborg {
pub use crate::tagger; pub use crate::tagger;
pub use crate::tasks; pub use crate::tasks;
pub use crate::test_scratch; pub use crate::test_scratch;
pub use crate::vcs;
pub use crate::worker; pub use crate::worker;
pub use crate::writetoline; pub use crate::writetoline;

View file

@ -1,7 +1,5 @@
use crate::message::{Pr, Repo}; use crate::message::{Pr, Repo};
use hubcaps::checks::Conclusion;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum BuildStatus { pub enum BuildStatus {
Skipped, Skipped,
@ -25,19 +23,6 @@ impl From<BuildStatus> for String {
} }
} }
impl From<BuildStatus> for Conclusion {
fn from(status: BuildStatus) -> Conclusion {
match status {
BuildStatus::Skipped => Conclusion::Skipped,
BuildStatus::Success => Conclusion::Success,
BuildStatus::Failure => Conclusion::Neutral,
BuildStatus::HashMismatch => Conclusion::Failure,
BuildStatus::TimedOut => Conclusion::Neutral,
BuildStatus::UnexpectedError { .. } => Conclusion::Neutral,
}
}
}
pub struct LegacyBuildResult { pub struct LegacyBuildResult {
pub repo: Repo, pub repo: Repo,
pub pr: Pr, pub pr: Pr,

View file

@ -9,8 +9,7 @@ use crate::checkout::CachedProjectCo;
use crate::commitstatus::{CommitStatus, CommitStatusError}; use crate::commitstatus::{CommitStatus, CommitStatusError};
use crate::evalchecker::EvalChecker; use crate::evalchecker::EvalChecker;
use crate::message::buildjob::BuildJob; use crate::message::buildjob::BuildJob;
use crate::vcs::generic::CheckRunOptions;
use hubcaps::checks::CheckRunOptions;
use std::path::Path; use std::path::Path;

40
ofborg/src/vcs/generic.rs Normal file
View file

@ -0,0 +1,40 @@
/// Set of generic structures to abstract over a VCS in a richful way.
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CheckRunState {
Queued,
InProgress,
Completed,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Conclusion {
Skipped,
Success,
Failure,
Neutral,
Cancelled,
TimedOut,
ActionRequired,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct CheckRunOptions {
pub name: String,
pub head_sha: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<CheckRunState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conclusion: Option<Conclusion>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
}

View file

@ -0,0 +1,318 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct Account {
pub name: Option<String>, // User's full name, if configured
pub email: Option<String>, // User's preferred email address
pub username: Option<String>, // User's username, if configured
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Approval {
pub r#type: String, // Internal name of the approval
pub description: String, // Human-readable category of the approval
pub value: i32, // Value assigned by the approval (usually a numerical score)
#[serde(rename = "oldValue")]
pub old_value: Option<i32>, // Previous approval score, if present
#[serde(rename = "grantedOn")]
pub granted_on: u64, // Time in seconds since the UNIX epoch
pub by: Account, // Reviewer of the patch set
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum ChangeType {
Added, // The file is being created/introduced by this patch
Modified, // The file already exists and has updated content
Deleted, // The file existed, but is being removed by this patch
Renamed, // The file is renamed
Copied, // The file is copied from another file
Rewrite, // The file was rewritten
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PatchFile {
pub file: String, // Name of the file, or new name if renamed
#[serde(rename = "fileOld")]
pub file_old: Option<String>, // Old name of the file, if renamed
pub r#type: ChangeType, // Type of change (ADDED, MODIFIED, DELETED, etc.)
pub insertions: i64, // Number of insertions in the patch
pub deletions: i64, // Number of deletions in the patch
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PatchSetComment {
pub file: String, // Name of the file on which the comment was added
pub line: u64, // Line number at which the comment was added
pub reviewer: Account, // Account that added the comment
pub message: String, // Comment text
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Kind {
Rework, // Nontrivial content changes
TrivialRebase, // Conflict-free merge with same commit message
TrivialRebaseWithMessageUpdate, // Conflict-free merge with message update
MergeFirstParentUpdate, // Conflict-free change of the first (left) parent
NoCodeChange, // No code change; same tree and parent tree
NoChange, // No changes; same commit message and tree
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PatchSet {
pub number: u64, // Patchset number
pub revision: String, // Git commit for this patchset
pub parents: Vec<String>, // List of parent revisions
pub r#ref: String, // Git reference pointing at the revision
pub uploader: Account, // Uploader of the patchset
pub author: Account, // Author of the patchset
#[serde(rename = "createdOn")]
pub created_on: u64, // Time in seconds since the UNIX epoch
pub kind: Kind, // Kind of change ("REWORK", "TRIVIAL_REBASE", etc.)
pub approvals: Vec<Approval>, // Approvals granted
pub comments: Vec<PatchSetComment>, // All comments for this patchset
pub files: Vec<String>, // All changed files in this patchset
#[serde(rename = "sizeInsertions")]
pub size_insertions: i64, // Size of insertions
#[serde(rename = "sizeDeletions")]
pub size_deletions: i64, // Size of deletions
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ReviewerMessage {
#[serde(rename = "message")]
pub comment_text: String, // Comment text added by the reviewer
pub timestamp: u64, // Time in seconds since the UNIX epoch when this comment was added
pub reviewer: Account, // Account of the reviewer who added the comment
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TrackingId {
pub system: String, // Name of the system from the gerrit.config file
pub id: String, // ID number scraped out of the commit message
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ChangeDependency {
pub id: String, // Change identifier
pub number: u64, // Change number
pub revision: String, // Patchset revision
pub r#ref: String, // Ref name
#[serde(rename = "isCurrentPatchSet")]
pub is_current_patch_set: bool, // If the revision is the current patchset of the change
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum ChangeStatus {
New, // Change is still being reviewed
Merged, // Change has been merged to its branch
Abandoned, // Change was abandoned by its owner or administrator
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum SubmitStatus {
Ok, // The change is ready for submission or already submitted
NotReady, // The change is missing a required label
RuleError, // An internal server error occurred preventing computation
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Label {
pub label: String, // Name of the label
pub status: LabelStatus, // Status of the label
pub by: Account, // The account that applied the label
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum LabelStatus {
Ok, // This label provides what is necessary for submission
Reject, // This label prevents the change from being submitted
Need, // The label is required for submission but has not been satisfied
May, // The label may be set but isnt necessary for submission
Impossible, // The label is required but impossible to complete
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Requirement {
#[serde(rename = "fallbackText")]
pub fallback_text: String, // Human-readable description of the requirement
pub r#type: String, // Alphanumerical string identifying the requirement
pub data: Option<HashMap<String, String>>, // Additional key-value data linked to this requirement
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SubmitRecord {
pub status: SubmitStatus, // Current submit status
pub labels: Option<Vec<Label>>, // State of each code review label attribute
pub requirements: Vec<Requirement>, // Requirements for submission
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Change {
pub project: String,
pub branch: String,
pub topic: Option<String>,
pub id: String,
#[serde(rename = "number")]
pub change_number: Option<u64>, // Deprecated, but keeping it optional
pub subject: String,
pub owner: Account,
pub url: String,
pub commit_message: String,
pub hashtags: Vec<String>,
#[serde(rename = "createdOn")]
pub created_on: u64, // Time in seconds since UNIX epoch
#[serde(rename = "lastUpdated")]
pub last_updated: u64, // Time in seconds since UNIX epoch
pub open: bool,
pub status: ChangeStatus, // "NEW", "MERGED", or "ABANDONED"
pub private: bool,
pub wip: bool, // Work in progress
pub comments: Vec<ReviewerMessage>, // Inline/file comments
#[serde(rename = "trackingIds")]
pub tracking_ids: Vec<TrackingId>, // Links to issue tracking systems
#[serde(rename = "currentPatchSet")]
pub current_patch_set: PatchSet,
#[serde(rename = "patchSets")]
pub patch_sets: Vec<PatchSet>, // All patch sets
#[serde(rename = "dependsOn")]
pub depends_on: Vec<ChangeDependency>, // Dependencies
#[serde(rename = "neededBy")]
pub needed_by: Vec<ChangeDependency>, // Reverse dependencies
#[serde(rename = "submitRecords")]
pub submit_records: Vec<SubmitRecord>, // Submission information
#[serde(rename = "allReviewers")]
pub all_reviewers: Vec<Account>, // List of all reviewers
}
#[derive(Serialize, Deserialize, Debug)]
pub struct RefUpdate {
#[serde(rename = "oldRev")]
pub old_rev: String, // The old value of the ref, prior to the update
#[serde(rename = "newRev")]
pub new_rev: String, // The new value the ref was updated to
pub ref_name: String, // Full ref name within the project
pub project: String, // Project path in Gerrit
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum GerritStreamEvent {
ChangeAbandoned {
change: Change,
patch_set: PatchSet,
abandoner: Account,
reason: String,
event_created_on: u64
},
ChangeDeleted {
change: Change,
deleter: Account
},
ChangeMerged {
change: Change,
patch_set: PatchSet,
submitter: Account,
new_rev: String,
event_created_on: u64
},
ChangeRestored {
change: Change,
patch_set: PatchSet,
restorer: Account,
reason: String,
event_created_on: u64
},
CommentAdded {
change: Change,
patch_set: PatchSet,
author: Account,
approvals: Vec<Approval>,
comment: String,
event_created_on: u64
},
DroppedOutput,
HashtagsChanged {
change: Change,
editor: Account,
added: Vec<String>,
removed: Vec<String>,
hashtags: Vec<String>,
event_created_on: u64,
},
ProjectCreated {
project_name: String,
project_head: String,
event_created_on: u64
},
PatchSetCreated {
change: Change,
patch_set: PatchSet,
uploader: Account,
event_created_on: u64
},
RefUpdated {
submitter: Account,
ref_update: RefUpdate,
event_created_on: u64
},
BatchRefUpdated {
submitter: Account,
ref_updates: Vec<RefUpdate>,
event_created_on: u64
},
ReviewerAdded {
change: Change,
patch_set: PatchSet,
reviewer: Account,
adder: Account,
event_created_on: u64
},
ReviewerDeleted {
change: Change,
patch_set: PatchSet,
reviewer: Account,
remover: Account,
approvals: Vec<Approval>,
comment: String,
event_created_on: u64,
},
TopicChanged {
change: Change,
old_topic: Option<String>,
new_topic: Option<String>,
changer: Account,
event_created_on: u64
},
WorkInProgressStateChanged {
change: Change,
patch_set: PatchSet,
changer: Account,
event_created_on: u64
},
PrivateStateChanged {
change: Change,
patch_set: PatchSet,
changer: Account,
event_created_on: u64
},
VoteDeleted {
change: Change,
patch_set: PatchSet,
reviewer: Account,
remover: Account,
approvals: Vec<Approval>,
comment: String,
},
ProjectHeadUpdate {
old_head: String,
new_head: String,
event_created_on: u64
}
}

View file

@ -0,0 +1,45 @@
use futures_util::stream::try_unfold;
use futures_util::Stream;
use reqwest::{Client, Response, Error};
use super::data_structures::GerritStreamEvent;
/// Streams Gerrit events back to the caller.
async fn stream_events(gerrit_baseurl: &str) -> Result<impl Stream<Item = Result<GerritStreamEvent, Box<dyn Error>>>, Box<dyn Error>> {
let client = Client::new();
// Send the request and get a response
let response: Response = client.get(format!("{}/stream-events", gerrit_baseurl)).send().await?;
// Ensure we are getting a successful response
if !response.status().is_success() {
return Err(format!("Failed to connect: {}", response.status()).into());
}
// Create a stream from the response body
let mut body_stream = response.bytes_stream();
// Create a stream of GerritStreamEvent from the body stream
let stream = try_unfold(body_stream, |mut body_stream| async move {
while let Some(item) = body_stream.next().await {
let bytes = item?;
let line = String::from_utf8_lossy(&bytes).to_string();
let event: Result<GerritStreamEvent, _> = serde_json::from_str(&line);
match event {
Ok(event) => {
return Ok(Some((event, body_stream)));
},
Err(err) => {
eprintln!("Failed to deserialize event: {:?}", line);
return Err(Box::new(err));
}
}
}
Ok(None) // End of stream
});
Ok(stream)
}

View file

@ -0,0 +1,2 @@
pub mod data_structures;
pub mod events;

141
ofborg/src/vcs/github.rs Normal file
View file

@ -0,0 +1,141 @@
use std::{collections::HashMap, path::PathBuf};
use async_std::io::BufReader;
use hubcaps::{checks::Conclusion, Credentials, Github, InstallationTokenGenerator, JWTCredentials};
use tracing::{debug, info, warn};
use crate::{config::Config, message::buildresult::BuildStatus, nix::File};
#[derive(Debug)]
pub enum CommitStatusError {
ExpiredCreds(hubcaps::Error),
MissingSha(hubcaps::Error),
Error(hubcaps::Error),
}
impl From<hubcaps::Error> for CommitStatusError {
fn from(e: hubcaps::Error) -> CommitStatusError {
use http::status::StatusCode;
use hubcaps::Error;
match &e {
Error::Fault { code, error }
if code == &StatusCode::UNAUTHORIZED && error.message == "Bad credentials" =>
{
CommitStatusError::ExpiredCreds(e)
}
Error::Fault { code, error }
if code == &StatusCode::UNPROCESSABLE_ENTITY
&& error.message.starts_with("No commit found for SHA:") =>
{
CommitStatusError::MissingSha(e)
}
_otherwise => CommitStatusError::Error(e),
}
}
}
impl From<BuildStatus> for Conclusion {
fn from(status: BuildStatus) -> Conclusion {
match status {
BuildStatus::Skipped => Conclusion::Skipped,
BuildStatus::Success => Conclusion::Success,
BuildStatus::Failure => Conclusion::Neutral,
BuildStatus::HashMismatch => Conclusion::Failure,
BuildStatus::TimedOut => Conclusion::Neutral,
BuildStatus::UnexpectedError { .. } => Conclusion::Neutral,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GithubConfig {
pub token_file: PathBuf,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GithubAppConfig {
pub app_id: u64,
pub private_key: PathBuf,
}
pub struct GithubAppVendingMachine {
conf: GithubAppConfig,
id_cache: HashMap<(String, String), Option<u64>>,
client_cache: HashMap<u64, Github>,
}
impl Config {
pub fn github(&self) -> Github {
let token = std::fs::read_to_string(self.github.clone().unwrap().token_file)
.expect("Couldn't read from GitHub token file");
Github::new(
"github.com/grahamc/ofborg",
// tls configured hyper client
Credentials::Token(token),
)
.expect("Unable to create a github client instance")
}
pub fn github_app_vendingmachine(&self) -> GithubAppVendingMachine {
GithubAppVendingMachine {
conf: self.github_app.clone().unwrap(),
id_cache: HashMap::new(),
client_cache: HashMap::new(),
}
}
}
impl GithubAppVendingMachine {
fn useragent(&self) -> &'static str {
"github.com/grahamc/ofborg (app)"
}
fn jwt(&self) -> JWTCredentials {
let private_key_file =
File::open(self.conf.private_key.clone()).expect("Unable to read private_key");
let mut private_key_reader = BufReader::new(private_key_file);
let private_keys = rustls_pemfile::rsa_private_keys(&mut private_key_reader)
.expect("Unable to convert private_key to DER format");
// We can be reasonably certain that there will only be one private key in this file
let private_key = &private_keys[0];
JWTCredentials::new(self.conf.app_id, private_key.to_vec())
.expect("Unable to create JWTCredentials")
}
fn install_id_for_repo(&mut self, owner: &str, repo: &str) -> Option<u64> {
let useragent = self.useragent();
let jwt = self.jwt();
let key = (owner.to_owned(), repo.to_owned());
*self.id_cache.entry(key).or_insert_with(|| {
info!("Looking up install ID for {}/{}", owner, repo);
let lookup_gh = Github::new(useragent, Credentials::JWT(jwt)).unwrap();
match async_std::task::block_on(lookup_gh.app().find_repo_installation(owner, repo)) {
Ok(install_id) => {
debug!("Received install ID {:?}", install_id);
Some(install_id.id)
}
Err(e) => {
warn!("Error during install ID lookup: {:?}", e);
None
}
}
})
}
pub fn for_repo<'a>(&'a mut self, owner: &str, repo: &str) -> Option<&'a Github> {
let useragent = self.useragent();
let jwt = self.jwt();
let install_id = self.install_id_for_repo(owner, repo)?;
Some(self.client_cache.entry(install_id).or_insert_with(|| {
Github::new(
useragent,
Credentials::InstallationToken(InstallationTokenGenerator::new(install_id, jwt)),
)
.expect("Unable to create a github client instance")
}))
}
}

3
ofborg/src/vcs/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod github;
pub mod generic;
pub mod gerrit;