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:
zhenyi
2026-06-08 14:31:29 +08:00
parent d243dce027
commit 8f472a0443
37 changed files with 4691 additions and 83 deletions
+238
View File
@@ -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"),
}
}
}