Skip to content

Commit

Permalink
Use github app auth for repo access.
Browse files Browse the repository at this point in the history
  • Loading branch information
bittrance committed Oct 31, 2023
1 parent 15e0bd5 commit 79d7a3b
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 78 deletions.
5 changes: 4 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{path::PathBuf, fmt::Debug};
use std::{fmt::Debug, path::PathBuf};

use thiserror::Error;

Expand Down Expand Up @@ -54,6 +54,9 @@ pub enum GitOpsError {
GitHubNetworkError(reqwest::Error),
#[error("GitHub App is installed but does not have write permissions for commit statuses")]
GitHubPermissionsError,
#[cfg(test)]
#[error("Test error")]
TestError,
}

impl GitOpsError {
Expand Down
101 changes: 84 additions & 17 deletions src/git.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use std::{path::Path, sync::atomic::AtomicBool, thread::scope, time::Instant};
use std::{
path::Path,
sync::{atomic::AtomicBool, Arc},
thread::scope,
time::Instant,
};

use gix::{
bstr::{BString, ByteSlice},
Expand All @@ -19,17 +24,12 @@ use crate::{errors::GitOpsError, opts::CliOptions, utils::Watchdog};
#[derive(Clone, Deserialize)]
pub struct GitConfig {
#[serde(deserialize_with = "url_from_string")]
url: Url,
pub url: Arc<Box<dyn UrlProvider>>,
#[serde(default = "GitConfig::default_branch")]
branch: String,
}

impl GitConfig {
pub fn safe_url(&self) -> String {
// TODO Change to whitelist of allowed characters
self.url.to_bstring().to_string().replace(['/', ':'], "_")
}

pub fn default_branch() -> String {
"main".to_owned()
}
Expand All @@ -41,18 +41,45 @@ impl TryFrom<&CliOptions> for GitConfig {
fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
let url = Url::try_from(opts.url.clone().unwrap()).map_err(GitOpsError::InvalidUrl)?;
Ok(GitConfig {
url,
url: Arc::new(Box::new(DefaultUrlProvider { url })),
branch: opts.branch.clone(),
})
}
}

fn url_from_string<'de, D>(deserializer: D) -> Result<Url, D::Error>
fn url_from_string<'de, D>(deserializer: D) -> Result<Arc<Box<dyn UrlProvider>>, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
Url::try_from(s).map_err(serde::de::Error::custom)
Ok(Arc::new(Box::new(DefaultUrlProvider {
url: Url::try_from(s).map_err(serde::de::Error::custom)?,
})))
}

pub trait UrlProvider: Send + Sync {
fn url(&self) -> &Url;
fn auth_url(&self) -> Result<Url, GitOpsError>;

fn safe_url(&self) -> String {
// TODO Change to whitelist of allowed characters
self.url().to_bstring().to_string().replace(['/', ':'], "_")
}
}

#[derive(Clone)]
pub struct DefaultUrlProvider {
url: Url,
}

impl UrlProvider for DefaultUrlProvider {
fn url(&self) -> &Url {
&self.url
}

fn auth_url(&self) -> Result<Url, GitOpsError> {
Ok(self.url.clone())
}
}

fn clone_repo(
Expand All @@ -63,13 +90,15 @@ fn clone_repo(
let watchdog = Watchdog::new(deadline);
scope(|s| {
s.spawn(watchdog.runner());
let repo = gix::prepare_clone(config.url.clone(), target)
.unwrap()
.fetch_only(Discard, &watchdog)
.map(|(r, _)| r)
.map_err(GitOpsError::InitRepo);
let maybe_repo = config.url.auth_url().and_then(|url| {
gix::prepare_clone(url, target)
.unwrap()
.fetch_only(Discard, &watchdog)
.map(|(r, _)| r)
.map_err(GitOpsError::InitRepo)
});
watchdog.cancel();
repo
maybe_repo
})
}

Expand All @@ -78,7 +107,7 @@ fn perform_fetch(
config: &GitConfig,
cancel: &AtomicBool,
) -> Result<Outcome, Box<dyn std::error::Error + Send + Sync>> {
repo.remote_at(config.url.clone())
repo.remote_at(config.url.auth_url()?)
.unwrap()
.with_refspecs([BString::from(config.branch.clone())], Direction::Fetch)
.unwrap()
Expand Down Expand Up @@ -181,3 +210,41 @@ where
};
checkout_worktree(&repo, &config.branch, workdir)
}

#[cfg(test)]
mod tests {
use std::{
sync::Arc,
time::{Duration, Instant},
};

use crate::{
errors::GitOpsError,
git::{clone_repo, fetch_repo, GitConfig},
testutils::TestUrl,
};

#[test]
fn clone_with_bad_url() {
let config = GitConfig {
url: Arc::new(Box::new(TestUrl::new(Some(GitOpsError::TestError)))),
branch: "main".into(),
};
let deadline = Instant::now() + Duration::from_secs(61); // Fail tests that time out
let target = tempfile::tempdir().unwrap();
let result = clone_repo(&config, deadline, target.path());
assert!(matches!(result, Err(GitOpsError::TestError)));
}

#[test]
fn fetch_with_bad_url() {
let repo = gix::open(".").unwrap();
let config = GitConfig {
url: Arc::new(Box::new(TestUrl::new(Some(GitOpsError::TestError)))),
branch: "main".into(),
};
let deadline = Instant::now() + Duration::from_secs(61); // Fail tests that time out
let result = fetch_repo(&repo, &config, deadline);
assert!(result.is_err());
}
}
15 changes: 11 additions & 4 deletions src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ use crate::{
receiver::logging_receiver,
store::{FileStore, Store},
task::{
github::github_watcher, gixworkload::GitWorkload, scheduled::ScheduledTask, GitTaskConfig,
github::{github_watcher, GithubUrlProvider},
gixworkload::GitWorkload,
scheduled::ScheduledTask,
GitTaskConfig,
},
};

Expand Down Expand Up @@ -97,10 +100,14 @@ struct ConfigFile {
}

fn into_task(mut config: GitTaskConfig, opts: &CliOptions) -> ScheduledTask<GitWorkload> {
let notify_config = config.notify.take();
let github = config.github.take();
if let Some(ref github) = github {
config
.upgrade_url_provider(|current| GithubUrlProvider::new(current.url().clone(), github));
}
let mut work = GitWorkload::from_config(config, opts);
if let Some(notify_config) = notify_config {
work.watch(github_watcher(notify_config));
if let Some(ref github) = github {
work.watch(github_watcher(github.clone()));
}
let (tx, rx) = channel();
work.watch(move |event| {
Expand Down
Loading

0 comments on commit 79d7a3b

Please sign in to comment.