feat(cluster): implement distributed clustering with etcd coordination
- Integrate etcd-client for distributed coordination and leader election - Add remote client macros with proper formatting for all services - Implement RequestMetrics for tracking RPC performance and errors - Add rate limiting mechanism across all service endpoints - Create ElectionRequest and ElectionResult message types for leader election - Add role management with primary/replica switching capabilities - Implement health checker with automatic failover detection - Add repository count metrics for cluster monitoring - Update Cargo.toml with etcd-client and dashmap dependencies - Modify RepoEntry to include read_only flag for replica handling - Implement should_accept_election logic to prevent duplicate elections - Add RoleChangedEvent handling for cluster role updates
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
//! Hook manager for GitKS.
|
||||
//!
|
||||
//! Manages the installation, listing, and removal of git hooks
|
||||
//! for bare repositories. Supports server hooks, custom hooks,
|
||||
//! and gRPC callback hooks.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::{GitError, GitResult};
|
||||
use crate::hooks::runner::{HookResult, generate_hook_runner_script, run_hook_dir};
|
||||
use crate::hooks::sanitize::{validate_hook_content, validate_hook_name};
|
||||
|
||||
/// Manages git hooks across repositories.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HookManager {
|
||||
repo_prefix: PathBuf,
|
||||
server_hooks_dir: Option<PathBuf>,
|
||||
hook_callback_addr: Option<String>,
|
||||
hook_timeout: Duration,
|
||||
allow_custom_hooks: bool,
|
||||
}
|
||||
|
||||
impl HookManager {
|
||||
pub fn new(
|
||||
repo_prefix: PathBuf,
|
||||
server_hooks_dir: Option<PathBuf>,
|
||||
hook_callback_addr: Option<String>,
|
||||
hook_timeout: Duration,
|
||||
allow_custom_hooks: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo_prefix,
|
||||
server_hooks_dir,
|
||||
hook_callback_addr,
|
||||
hook_timeout,
|
||||
allow_custom_hooks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Install gitks hook runner scripts into a repository's hooks directory.
|
||||
/// Called during repository initialization.
|
||||
pub fn install_hooks(&self, repo_path: &Path) -> GitResult<()> {
|
||||
let hooks_dir = repo_path.join("hooks");
|
||||
std::fs::create_dir_all(&hooks_dir).map_err(GitError::Io)?;
|
||||
|
||||
let relative_path = repo_path
|
||||
.strip_prefix(&self.repo_prefix)
|
||||
.unwrap_or(repo_path)
|
||||
.to_string_lossy()
|
||||
.trim_start_matches('/')
|
||||
.to_string();
|
||||
|
||||
let server_hooks_dir_str = self
|
||||
.server_hooks_dir
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().into_owned());
|
||||
let callback_addr_str = self.hook_callback_addr.clone();
|
||||
|
||||
for hook_type in &["pre-receive", "update", "post-receive"] {
|
||||
let script_content = generate_hook_runner_script(
|
||||
hook_type,
|
||||
&relative_path,
|
||||
server_hooks_dir_str.as_deref(),
|
||||
callback_addr_str.as_deref(),
|
||||
self.hook_timeout.as_secs(),
|
||||
);
|
||||
let hook_path = hooks_dir.join(hook_type);
|
||||
std::fs::write(&hook_path, script_content).map_err(GitError::Io)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&hook_path)
|
||||
.map_err(GitError::Io)?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&hook_path, perms).map_err(GitError::Io)?;
|
||||
}
|
||||
tracing::debug!(
|
||||
hook_type = %hook_type,
|
||||
path = %hook_path.display(),
|
||||
"installed gitks hook runner"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
path = %repo_path.display(),
|
||||
"hooks installed for repository"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set a custom hook script for a repository.
|
||||
pub fn set_custom_hook(
|
||||
&self,
|
||||
repo_path: &Path,
|
||||
hook_name: &str,
|
||||
content: &str,
|
||||
) -> GitResult<()> {
|
||||
validate_hook_name(hook_name)?;
|
||||
if !self.allow_custom_hooks {
|
||||
return Err(GitError::PermissionDenied(
|
||||
"custom hooks are not allowed on this server".into(),
|
||||
));
|
||||
}
|
||||
validate_hook_content(content)?;
|
||||
|
||||
let custom_hooks_dir = repo_path.join("custom_hooks").join(hook_name).join("d");
|
||||
std::fs::create_dir_all(&custom_hooks_dir).map_err(GitError::Io)?;
|
||||
|
||||
let script_name = format!("gitks_custom_{}", hook_name.replace('-', "_"));
|
||||
let script_path = custom_hooks_dir.join(&script_name);
|
||||
std::fs::write(&script_path, content).map_err(GitError::Io)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&script_path)
|
||||
.map_err(GitError::Io)?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&script_path, perms).map_err(GitError::Io)?;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
repo = %repo_path.display(),
|
||||
hook_name = %hook_name,
|
||||
"custom hook set"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a custom hook from a repository.
|
||||
pub fn remove_custom_hook(&self, repo_path: &Path, hook_name: &str) -> GitResult<()> {
|
||||
validate_hook_name(hook_name)?;
|
||||
let custom_hooks_dir = repo_path.join("custom_hooks").join(hook_name).join("d");
|
||||
if custom_hooks_dir.exists() {
|
||||
std::fs::remove_dir_all(&custom_hooks_dir).map_err(GitError::Io)?;
|
||||
tracing::info!(
|
||||
repo = %repo_path.display(),
|
||||
hook_name = %hook_name,
|
||||
"custom hook removed"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all hooks for a repository.
|
||||
pub fn list_hooks(&self, repo_path: &Path) -> GitResult<Vec<HookInfo>> {
|
||||
let mut hooks = Vec::new();
|
||||
|
||||
let hooks_dir = repo_path.join("hooks");
|
||||
if hooks_dir.exists() {
|
||||
for hook_type in &["pre-receive", "update", "post-receive"] {
|
||||
let hook_path = hooks_dir.join(hook_type);
|
||||
if hook_path.exists() {
|
||||
hooks.push(HookInfo {
|
||||
hook_type: hook_type.to_string(),
|
||||
level: HookLevel::Server,
|
||||
path: hook_path.to_string_lossy().into_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.allow_custom_hooks {
|
||||
let custom_dir = repo_path.join("custom_hooks");
|
||||
if custom_dir.exists() {
|
||||
for entry in std::fs::read_dir(&custom_dir).map_err(GitError::Io)? {
|
||||
let entry = entry.map_err(GitError::Io)?;
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if validate_hook_name(&name).is_ok() {
|
||||
let path = entry.path();
|
||||
let d_dir = path.join("d");
|
||||
if d_dir.exists() {
|
||||
let script_count =
|
||||
std::fs::read_dir(&d_dir).map_err(GitError::Io)?.count();
|
||||
if script_count > 0 {
|
||||
hooks.push(HookInfo {
|
||||
hook_type: name,
|
||||
level: HookLevel::Custom,
|
||||
path: d_dir.to_string_lossy().into_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hooks)
|
||||
}
|
||||
|
||||
/// Execute a hook by running server hooks then custom hooks.
|
||||
pub fn execute_hook(&self, repo_path: &Path, hook_type: &str, stdin_data: &[u8]) -> HookResult {
|
||||
if let Some(ref server_dir) = self.server_hooks_dir {
|
||||
let dir = server_dir.join(hook_type).join("d");
|
||||
let result = run_hook_dir(&dir, hook_type, stdin_data, self.hook_timeout);
|
||||
if !result.accepted {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if self.allow_custom_hooks {
|
||||
let custom_dir = repo_path.join("custom_hooks").join(hook_type).join("d");
|
||||
let result = run_hook_dir(&custom_dir, hook_type, stdin_data, self.hook_timeout);
|
||||
if !result.accepted {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
HookResult::accepted()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a hook.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HookInfo {
|
||||
pub hook_type: String,
|
||||
pub level: HookLevel,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Hook level (server vs custom).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum HookLevel {
|
||||
Server,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HookLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
HookLevel::Server => write!(f, "server"),
|
||||
HookLevel::Custom => write!(f, "custom"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user