[O] Webhook install respect filters
This commit is contained in:
+15
-3
@@ -15,7 +15,8 @@ use sha2::Sha256;
|
|||||||
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
|
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
|
||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
Config, EndpointConfig, MirrorConfig, ProviderKind, default_work_dir, validate_config,
|
Config, EndpointConfig, MirrorConfig, ProviderKind, RepoNameFilter, default_work_dir,
|
||||||
|
validate_config,
|
||||||
};
|
};
|
||||||
use crate::provider::{EndpointRepo, ProviderClient, RemoteRepo};
|
use crate::provider::{EndpointRepo, ProviderClient, RemoteRepo};
|
||||||
use crate::state::{load_toml_or_default, save_toml};
|
use crate::state::{load_toml_or_default, save_toml};
|
||||||
@@ -201,8 +202,7 @@ pub fn install_webhooks(config: &Config, options: WebhookInstallOptions) -> Resu
|
|||||||
.with_context(|| format!("failed to list repos for {}", endpoint.label()))?;
|
.with_context(|| format!("failed to list repos for {}", endpoint.label()))?;
|
||||||
for repo in repos
|
for repo in repos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|repo| mirror.sync_visibility.matches_private(repo.private))
|
.filter(|repo| webhook_repo_matches(mirror, &repo_filter, repo))
|
||||||
.filter(|repo| repo_filter.matches(&repo.name))
|
|
||||||
{
|
{
|
||||||
tasks.push(WebhookInstallTask {
|
tasks.push(WebhookInstallTask {
|
||||||
site: site.clone(),
|
site: site.clone(),
|
||||||
@@ -326,8 +326,12 @@ pub fn ensure_configured_webhooks(
|
|||||||
}
|
}
|
||||||
let secret = webhook.secret()?;
|
let secret = webhook.secret()?;
|
||||||
let state = Arc::new(Mutex::new(load_webhook_state(work_dir)?));
|
let state = Arc::new(Mutex::new(load_webhook_state(work_dir)?));
|
||||||
|
let repo_filter = mirror.repo_filter()?;
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
for endpoint_repo in repos {
|
for endpoint_repo in repos {
|
||||||
|
if !webhook_repo_matches(mirror, &repo_filter, &endpoint_repo.repo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let Some(site) = config.site(&endpoint_repo.endpoint.site) else {
|
let Some(site) = config.site(&endpoint_repo.endpoint.site) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -348,6 +352,14 @@ pub fn ensure_configured_webhooks(
|
|||||||
save_webhook_state(work_dir, &state)
|
save_webhook_state(work_dir, &state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn webhook_repo_matches(
|
||||||
|
mirror: &MirrorConfig,
|
||||||
|
repo_filter: &RepoNameFilter,
|
||||||
|
repo: &RemoteRepo,
|
||||||
|
) -> bool {
|
||||||
|
mirror.sync_visibility.matches_private(repo.private) && repo_filter.matches(&repo.name)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_webhook_url_reachable(url: &str) -> Result<()> {
|
pub fn check_webhook_url_reachable(url: &str) -> Result<()> {
|
||||||
let client = reqwest::blocking::Client::builder()
|
let client = reqwest::blocking::Client::builder()
|
||||||
.timeout(Duration::from_secs(10))
|
.timeout(Duration::from_secs(10))
|
||||||
|
|||||||
+147
-1
@@ -2,7 +2,7 @@ use super::*;
|
|||||||
use crate::config::SyncVisibility;
|
use crate::config::SyncVisibility;
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
ConflictResolutionStrategy, EndpointConfig, MirrorConfig, NamespaceKind, SiteConfig,
|
ConflictResolutionStrategy, EndpointConfig, MirrorConfig, NamespaceKind, SiteConfig,
|
||||||
TokenConfig, Visibility,
|
TokenConfig, Visibility, WebhookConfig,
|
||||||
};
|
};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
@@ -169,6 +169,124 @@ fn matching_jobs_respects_repo_name_filters() {
|
|||||||
assert_eq!(matching_jobs(&config, &webhook_event("random")).len(), 1);
|
assert_eq!(matching_jobs(&config, &webhook_event("random")).len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_webhooks_respects_visibility_and_repo_name_filters() {
|
||||||
|
let repos = r#"[
|
||||||
|
{"name":"important-api","clone_url":"https://github.com/alice/important-api.git","private":false,"description":null,"owner":{"login":"alice"}},
|
||||||
|
{"name":"important-private","clone_url":"https://github.com/alice/important-private.git","private":true,"description":null,"owner":{"login":"alice"}},
|
||||||
|
{"name":"important-archive","clone_url":"https://github.com/alice/important-archive.git","private":false,"description":null,"owner":{"login":"alice"}},
|
||||||
|
{"name":"random","clone_url":"https://github.com/alice/random.git","private":false,"description":null,"owner":{"login":"alice"}}
|
||||||
|
]"#;
|
||||||
|
let (api_url, handle) = request_server(
|
||||||
|
vec![
|
||||||
|
("200 OK", repos),
|
||||||
|
("200 OK", "[]"),
|
||||||
|
("200 OK", "[]"),
|
||||||
|
("201 Created", r#"{"id":1}"#),
|
||||||
|
],
|
||||||
|
|index, request| match index {
|
||||||
|
0 => assert!(
|
||||||
|
request
|
||||||
|
.starts_with("GET /user/repos?affiliation=owner&visibility=all&per_page=100 "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
1 => assert!(
|
||||||
|
request
|
||||||
|
.starts_with("GET /user/repos?affiliation=owner&visibility=all&per_page=100 "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
2 => assert!(
|
||||||
|
request.starts_with("GET /repos/alice/important-api/hooks "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
3 => assert!(
|
||||||
|
request.starts_with("POST /repos/alice/important-api/hooks "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
|
sites: vec![
|
||||||
|
SiteConfig {
|
||||||
|
api_url: Some(api_url.clone()),
|
||||||
|
..site("github", ProviderKind::Github)
|
||||||
|
},
|
||||||
|
SiteConfig {
|
||||||
|
api_url: Some(api_url),
|
||||||
|
..site("github-peer", ProviderKind::Github)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mirrors: vec![filtered_mirror()],
|
||||||
|
webhook: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
install_webhooks(
|
||||||
|
&config,
|
||||||
|
WebhookInstallOptions {
|
||||||
|
url: "https://mirror.example.test/webhook".to_string(),
|
||||||
|
secret: "secret".to_string(),
|
||||||
|
dry_run: false,
|
||||||
|
work_dir: Some(temp.path().to_path_buf()),
|
||||||
|
jobs: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_webhook_install_respects_visibility_and_repo_name_filters() {
|
||||||
|
let (api_url, handle) = request_server(
|
||||||
|
vec![("200 OK", "[]"), ("201 Created", r#"{"id":1}"#)],
|
||||||
|
|index, request| match index {
|
||||||
|
0 => assert!(
|
||||||
|
request.starts_with("GET /repos/alice/important-api/hooks "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
1 => assert!(
|
||||||
|
request.starts_with("POST /repos/alice/important-api/hooks "),
|
||||||
|
"request was {request}"
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let mirror = filtered_mirror();
|
||||||
|
let endpoint = mirror.endpoints[0].clone();
|
||||||
|
let config = Config {
|
||||||
|
jobs: crate::config::DEFAULT_JOBS,
|
||||||
|
sites: vec![
|
||||||
|
SiteConfig {
|
||||||
|
api_url: Some(api_url),
|
||||||
|
..site("github", ProviderKind::Github)
|
||||||
|
},
|
||||||
|
site("github-peer", ProviderKind::Github),
|
||||||
|
],
|
||||||
|
mirrors: vec![mirror.clone()],
|
||||||
|
webhook: Some(WebhookConfig {
|
||||||
|
install: true,
|
||||||
|
url: "https://mirror.example.test/webhook".to_string(),
|
||||||
|
secret: TokenConfig::Value("secret".to_string()),
|
||||||
|
full_sync_interval_minutes: None,
|
||||||
|
reachability_check_interval_minutes: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let repos = vec![
|
||||||
|
endpoint_repo(&endpoint, "important-api", false),
|
||||||
|
endpoint_repo(&endpoint, "important-private", true),
|
||||||
|
endpoint_repo(&endpoint, "important-archive", false),
|
||||||
|
endpoint_repo(&endpoint, "random", false),
|
||||||
|
];
|
||||||
|
|
||||||
|
ensure_configured_webhooks(&config, &mirror, &repos, temp.path(), 1).unwrap();
|
||||||
|
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn webhook_state_persists_installations() {
|
fn webhook_state_persists_installations() {
|
||||||
let temp = tempfile::TempDir::new().unwrap();
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
@@ -414,6 +532,34 @@ fn site(name: &str, provider: ProviderKind) -> SiteConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filtered_mirror() -> MirrorConfig {
|
||||||
|
MirrorConfig {
|
||||||
|
name: "sync-1".to_string(),
|
||||||
|
endpoints: vec![
|
||||||
|
endpoint("github", NamespaceKind::User, "alice"),
|
||||||
|
endpoint("github-peer", NamespaceKind::User, "bob"),
|
||||||
|
],
|
||||||
|
sync_visibility: SyncVisibility::Public,
|
||||||
|
repo_whitelist: vec!["^important-".to_string()],
|
||||||
|
repo_blacklist: vec!["-archive$".to_string()],
|
||||||
|
create_missing: true,
|
||||||
|
visibility: Visibility::Private,
|
||||||
|
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_repo(endpoint: &EndpointConfig, name: &str, private: bool) -> EndpointRepo {
|
||||||
|
EndpointRepo {
|
||||||
|
endpoint: endpoint.clone(),
|
||||||
|
repo: RemoteRepo {
|
||||||
|
name: name.to_string(),
|
||||||
|
clone_url: format!("https://github.com/alice/{name}.git"),
|
||||||
|
private,
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn webhook_event(repo: &str) -> WebhookEvent {
|
fn webhook_event(repo: &str) -> WebhookEvent {
|
||||||
WebhookEvent {
|
WebhookEvent {
|
||||||
provider: Some(ProviderKind::Github),
|
provider: Some(ProviderKind::Github),
|
||||||
|
|||||||
Reference in New Issue
Block a user