Private
Public Access
1
0

fix: resolve CI failures (fmt, clippy, tests)
All checks were successful
CI/CD Pipeline / Code Format (pull_request) Successful in 4s
CI/CD Pipeline / Clippy Lints (pull_request) Successful in 47s
CI/CD Pipeline / All Unit Tests (pull_request) Successful in 1m22s
CI/CD Pipeline / Security Audit (pull_request) Successful in 5s
CI/CD Pipeline / Enrollment Tests (pull_request) Successful in 1m22s
CI/CD Pipeline / Verify Enrollment CLI Flag (pull_request) Successful in 1m27s
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (pull_request) Successful in 2m49s
CI/CD Pipeline / Build RPM Package (pull_request) Successful in 2m40s
CI/CD Pipeline / Build Arch Package (pull_request) Successful in 3m2s
CI/CD Pipeline / Build Debian Package (pull_request) Successful in 2m34s
CI/CD Pipeline / Build Alpine Package (pull_request) Successful in 4m3s

- Fix rustfmt formatting in cache.rs, patches.rs, system.rs, routes.rs, main.rs
- Add Default impl for PackageCacheState (clippy new_without_default)
- Change apply_with_cache_retry generic bound from Fn to FnMut
- Add mut to refresh_fn parameter for FnMut compatibility
- Replace bool comparison with ! operator (clippy bool_comparison)
- Update todo.md with completed status
This commit is contained in:
2026-05-27 15:04:25 -05:00
parent cf3d597480
commit 6f63eeed57
6 changed files with 137 additions and 72 deletions

View File

@ -126,7 +126,12 @@ pub async fn apply_patches(
// MANDATORY: Refresh package cache before applying patches // MANDATORY: Refresh package cache before applying patches
let _ = job_manager_clone let _ = job_manager_clone
.update_job(&job_id_clone, JobStatus::Running, Some(0), Some("Refreshing package index...".to_string())) .update_job(
&job_id_clone,
JobStatus::Running,
Some(0),
Some("Refreshing package index...".to_string()),
)
.await; .await;
let _ = job_manager_clone let _ = job_manager_clone
.add_job_log(&job_id_clone, "Refreshing package cache...".to_string()) .add_job_log(&job_id_clone, "Refreshing package cache...".to_string())
@ -135,10 +140,18 @@ pub async fn apply_patches(
match backend_clone.refresh_package_cache(&cache_state_clone) { match backend_clone.refresh_package_cache(&cache_state_clone) {
Ok(_) => { Ok(_) => {
let _ = job_manager_clone let _ = job_manager_clone
.add_job_log(&job_id_clone, "Package cache refreshed successfully".to_string()) .add_job_log(
&job_id_clone,
"Package cache refreshed successfully".to_string(),
)
.await; .await;
let _ = job_manager_clone let _ = job_manager_clone
.update_job(&job_id_clone, JobStatus::Running, Some(10), Some("Cache refreshed, applying patches...".to_string())) .update_job(
&job_id_clone,
JobStatus::Running,
Some(10),
Some("Cache refreshed, applying patches...".to_string()),
)
.await; .await;
} }
Err(e) => { Err(e) => {
@ -194,17 +207,25 @@ pub async fn apply_patches(
// 404/fetch error: refresh cache and retry once // 404/fetch error: refresh cache and retry once
info!(job_id = %job_id_clone, "Patch apply failed with fetch error, refreshing cache and retrying"); info!(job_id = %job_id_clone, "Patch apply failed with fetch error, refreshing cache and retrying");
let _ = job_manager_clone let _ = job_manager_clone
.add_job_log(&job_id_clone, "Fetch error detected, refreshing cache and retrying...".to_string()) .add_job_log(
&job_id_clone,
"Fetch error detected, refreshing cache and retrying..."
.to_string(),
)
.await; .await;
match backend_clone.refresh_package_cache(&cache_state_clone) { match backend_clone.refresh_package_cache(&cache_state_clone) {
Ok(_) => { Ok(_) => {
let _ = job_manager_clone let _ = job_manager_clone
.add_job_log(&job_id_clone, "Cache refreshed, retrying patch apply...".to_string()) .add_job_log(
&job_id_clone,
"Cache refreshed, retrying patch apply...".to_string(),
)
.await; .await;
} }
Err(refresh_err) => { Err(refresh_err) => {
let err_msg = format!("Cache refresh on retry failed: {}", refresh_err); let err_msg =
format!("Cache refresh on retry failed: {}", refresh_err);
let _ = job_manager_clone.fail_job(&job_id_clone, err_msg).await; let _ = job_manager_clone.fail_job(&job_id_clone, err_msg).await;
error!(job_id = %job_id_clone, error = %refresh_err, "Cache refresh on retry failed"); error!(job_id = %job_id_clone, error = %refresh_err, "Cache refresh on retry failed");
return; return;
@ -228,29 +249,40 @@ pub async fn apply_patches(
), ),
) )
.await; .await;
match backend_clone.reboot_system(request.reboot_delay_seconds) { match backend_clone.reboot_system(request.reboot_delay_seconds)
{
Ok(_) => { Ok(_) => {
let _ = job_manager_clone let _ = job_manager_clone
.add_job_log(&job_id_clone, "Reboot command executed".to_string()) .add_job_log(
&job_id_clone,
"Reboot command executed".to_string(),
)
.await; .await;
} }
Err(e) => { Err(e) => {
let _ = job_manager_clone let _ = job_manager_clone
.add_job_log(&job_id_clone, format!("Reboot failed: {}", e)) .add_job_log(
&job_id_clone,
format!("Reboot failed: {}", e),
)
.await; .await;
} }
} }
} }
} }
Err(retry_err) => { Err(retry_err) => {
let _ = job_manager_clone.fail_job(&job_id_clone, retry_err.to_string()).await; let _ = job_manager_clone
.fail_job(&job_id_clone, retry_err.to_string())
.await;
error!(job_id = %job_id_clone, error = %retry_err, "Patch application failed after retry"); error!(job_id = %job_id_clone, error = %retry_err, "Patch application failed after retry");
} }
} }
} }
Err(e) => { Err(e) => {
// Non-fetch error: fail immediately // Non-fetch error: fail immediately
let _ = job_manager_clone.fail_job(&job_id_clone, e.to_string()).await; let _ = job_manager_clone
.fail_job(&job_id_clone, e.to_string())
.await;
error!(job_id = %job_id_clone, error = %e, "Patch application failed"); error!(job_id = %job_id_clone, error = %e, "Patch application failed");
} }
} }

View File

@ -42,11 +42,11 @@ pub struct SystemInfoData {
/// Health check response data /// Health check response data
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct HealthData { pub struct HealthData {
pub status: String, // "healthy" or "degraded" pub status: String, // "healthy" or "degraded"
pub uptime_seconds: u64, pub uptime_seconds: u64,
pub version: String, pub version: String,
pub last_cache_update: Option<String>, // RFC3339 timestamp pub last_cache_update: Option<String>, // RFC3339 timestamp
pub cache_status: String, // "fresh", "stale", "unknown", "failed" pub cache_status: String, // "fresh", "stale", "unknown", "failed"
} }
/// Service status response data /// Service status response data
@ -138,15 +138,27 @@ pub async fn health_check(
match backend.refresh_package_cache(&cache_state) { match backend.refresh_package_cache(&cache_state) {
Ok(_) => { Ok(_) => {
let updated = cache_state.status(); let updated = cache_state.status();
("healthy".to_string(), "fresh".to_string(), updated.last_update.map(|dt| dt.to_rfc3339())) (
"healthy".to_string(),
"fresh".to_string(),
updated.last_update.map(|dt| dt.to_rfc3339()),
)
} }
Err(e) => { Err(e) => {
error!("Health check cache refresh failed: {}", e); error!("Health check cache refresh failed: {}", e);
("degraded".to_string(), "failed".to_string(), cache_status_val.last_update.map(|dt| dt.to_rfc3339())) (
"degraded".to_string(),
"failed".to_string(),
cache_status_val.last_update.map(|dt| dt.to_rfc3339()),
)
} }
} }
} else { } else {
("healthy".to_string(), "fresh".to_string(), cache_status_val.last_update.map(|dt| dt.to_rfc3339())) (
"healthy".to_string(),
"fresh".to_string(),
cache_status_val.last_update.map(|dt| dt.to_rfc3339()),
)
}; };
let response = ApiResponse::success(HealthData { let response = ApiResponse::success(HealthData {

View File

@ -26,21 +26,24 @@ pub fn configure_api_routes(
) { ) {
info!("Configuring API v1 routes"); info!("Configuring API v1 routes");
cfg.app_data(job_manager).app_data(backend).app_data(cache_state).service( cfg.app_data(job_manager)
web::scope("/api/v1") .app_data(backend)
// VULN-005: Default handler for unsupported methods returns 405 instead of 404 .app_data(cache_state)
.default_service(web::route().to(method_not_allowed)) .service(
// Package Management Endpoints web::scope("/api/v1")
.configure(packages::configure_routes) // VULN-005: Default handler for unsupported methods returns 405 instead of 404
// Patch Management Endpoints .default_service(web::route().to(method_not_allowed))
.configure(patches::configure_routes) // Package Management Endpoints
// System Management Endpoints .configure(packages::configure_routes)
.configure(system::configure_routes) // Patch Management Endpoints
// Job Management Endpoints .configure(patches::configure_routes)
.configure(jobs::configure_routes) // System Management Endpoints
// WebSocket Endpoint .configure(system::configure_routes)
.configure(websocket::configure_routes), // Job Management Endpoints
); .configure(jobs::configure_routes)
// WebSocket Endpoint
.configure(websocket::configure_routes),
);
} }
/// Health check route (outside API scope for load balancer checks) /// Health check route (outside API scope for load balancer checks)

View File

@ -166,7 +166,12 @@ async fn main() -> Result<()> {
// Configure API routes // Configure API routes
app = app.configure(|cfg| { app = app.configure(|cfg| {
configure_api_routes(cfg, job_manager_data.clone(), backend_data.clone(), cache_state.clone()); configure_api_routes(
cfg,
job_manager_data.clone(),
backend_data.clone(),
cache_state.clone(),
);
}); });
// Configure health route (outside API scope) // Configure health route (outside API scope)

View File

@ -21,7 +21,7 @@ const CACHE_REFRESH_TIMEOUT_SECS: u64 = 120;
/// Persistent cache state (written to cache.json) /// Persistent cache state (written to cache.json)
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStateFile { pub struct CacheStateFile {
pub last_cache_update: Option<String>, // RFC3339 pub last_cache_update: Option<String>, // RFC3339
pub last_update_success: bool, pub last_update_success: bool,
} }
@ -44,6 +44,12 @@ struct CacheStateInner {
last_update_error: Option<String>, last_update_error: Option<String>,
} }
impl Default for PackageCacheState {
fn default() -> Self {
Self::new()
}
}
impl PackageCacheState { impl PackageCacheState {
pub fn new() -> Self { pub fn new() -> Self {
// Try to load from state file on startup // Try to load from state file on startup
@ -83,8 +89,7 @@ impl PackageCacheState {
Some(t) => { Some(t) => {
let threshold = Duration::from_secs(STALE_THRESHOLD_SECS); let threshold = Duration::from_secs(STALE_THRESHOLD_SECS);
Utc::now() - t Utc::now() - t
> chrono::Duration::from_std(threshold) > chrono::Duration::from_std(threshold).unwrap_or(chrono::TimeDelta::MAX)
.unwrap_or(chrono::TimeDelta::MAX)
} }
} }
} }
@ -151,12 +156,9 @@ pub fn is_fetch_error(error: &anyhow::Error) -> bool {
/// Execute a patch apply with automatic cache refresh retry on 404/fetch errors. /// Execute a patch apply with automatic cache refresh retry on 404/fetch errors.
/// Hardcoded 1 retry after cache refresh. /// Hardcoded 1 retry after cache refresh.
pub fn apply_with_cache_retry<F>( pub fn apply_with_cache_retry<F>(mut refresh_fn: F, apply_fn: impl Fn() -> Result<()>) -> Result<()>
refresh_fn: F,
apply_fn: impl Fn() -> Result<()>,
) -> Result<()>
where where
F: Fn() -> Result<()>, F: FnMut() -> Result<()>,
{ {
match apply_fn() { match apply_fn() {
Ok(()) => Ok(()), Ok(()) => Ok(()),
@ -226,7 +228,7 @@ mod tests {
let status = state.status(); let status = state.status();
// Fresh state should have no last_update (unless state file exists) // Fresh state should have no last_update (unless state file exists)
// Just verify it doesn't panic // Just verify it doesn't panic
assert!(status.last_update_success == false || status.last_update.is_some()); assert!(!status.last_update_success || status.last_update.is_some());
} }
#[test] #[test]
@ -259,19 +261,14 @@ mod tests {
#[test] #[test]
fn test_apply_with_cache_retry_success() { fn test_apply_with_cache_retry_success() {
let result = apply_with_cache_retry( let result = apply_with_cache_retry(|| Ok(()), || Ok(()));
|| Ok(()),
|| Ok(()),
);
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test] #[test]
fn test_apply_with_cache_retry_non_fetch_error() { fn test_apply_with_cache_retry_non_fetch_error() {
let result: Result<()> = apply_with_cache_retry( let result: Result<()> =
|| Ok(()), apply_with_cache_retry(|| Ok(()), || Err(anyhow::anyhow!("Permission denied")));
|| Err(anyhow::anyhow!("Permission denied")),
);
assert!(result.is_err()); assert!(result.is_err());
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!(!is_fetch_error(&err)); assert!(!is_fetch_error(&err));

View File

@ -2,33 +2,49 @@
**Spec:** tasks/issue-2-package-cache-refresh.md **Spec:** tasks/issue-2-package-cache-refresh.md
**Version:** 1.1.17 **Version:** 1.1.17
**Status:** In Progress **Status:** Complete - PR #3 Open
--- ---
## Implementation Checklist ## Implementation Checklist
- [ ] 1. Create `src/packages/cache.rs` - Core cache types, stale detection, state persistence, 404 retry logic - [x] 1. Create `src/packages/cache.rs` - Core cache types, stale detection, state persistence, 404 retry logic
- [ ] 2. Add `mod cache;` to `src/packages/mod.rs` - [x] 2. Add `mod cache;` to `src/packages/mod.rs`
- [ ] 3. Implement `refresh_package_cache()` on AptBackend - [x] 3. Implement `refresh_package_cache()` on AptBackend
- [ ] 4. Implement `refresh_package_cache()` on DnfBackend - [x] 4. Implement `refresh_package_cache()` on DnfBackend
- [ ] 5. Implement `refresh_package_cache()` on YumBackend - [x] 5. Implement `refresh_package_cache()` on YumBackend
- [ ] 6. Implement `refresh_package_cache()` on ApkBackend - [x] 6. Implement `refresh_package_cache()` on ApkBackend
- [ ] 7. Implement `refresh_package_cache()` on PacmanBackend - [x] 7. Implement `refresh_package_cache()` on PacmanBackend
- [ ] 8. Implement `last_cache_update()` on all backends (shared state) - [x] 8. Implement `last_cache_update()` on all backends (shared state)
- [ ] 9. Add `refresh_package_cache` and `last_cache_update` to PackageManagerBackend trait - [x] 9. Add `refresh_package_cache` and `last_cache_update` to PackageManagerBackend trait
- [ ] 10. Enhance health check in `src/api/handlers/system.rs` - add cache status, trigger refresh - [x] 10. Enhance health check in `src/api/handlers/system.rs` - add cache status, trigger refresh
- [ ] 11. Update HealthData struct with `last_cache_update` and `cache_status` fields - [x] 11. Update HealthData struct with `last_cache_update` and `cache_status` fields
- [ ] 12. Add pre-apply cache refresh in `src/api/handlers/patches.rs` - [x] 12. Add pre-apply cache refresh in `src/api/handlers/patches.rs`
- [ ] 13. Bump version in `Cargo.toml` to 1.1.17 - [x] 13. Bump version in `Cargo.toml` to 1.1.17
- [ ] 14. Update `ARCHITECTURE.md` with cache refresh flow - [x] 14. Update `ARCHITECTURE.md` with cache refresh flow
- [ ] 15. Update `REQUIREMENTS.md` with FR-007 - [x] 15. Update `REQUIREMENTS.md` with FR-007
- [ ] 16. Implement state file persistence (cache.json read/write) - [x] 16. Implement state file persistence (cache.json read/write)
- [ ] 17. Write unit tests for cache module - [x] 17. Write unit tests for cache module
- [ ] 18. Build and verify compilation - [x] 18. Build and verify compilation
- [ ] 19. Commit and push to fix/package-cache-refresh branch - [x] 19. Commit and push to fix/package-cache-refresh branch
- [ ] 20. Create PR and reference Issue #2 - [x] 20. Create PR and reference Issue #2
## Review ## Review
_To be filled after implementation_ **PR:** https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/pulls/3
**Branch:** fix/package-cache-refresh
**Commit:** cf3d597
**Files Changed:** 12 files, 944 insertions, 15 deletions
### Issue Resolution
All 4 requirements from Issue #2 addressed:
1. ✅ Pre-Upgrade Cache Refresh (MUST) - Mandatory cache refresh before every patch_apply
2. ✅ Regular Interval Cache Refresh (MUST) - Cache refresh triggered on health check when stale (>4h)
3. ✅ 404/Fetch Error Handling (SHOULD) - Auto-retry with cache refresh on fetch errors (1 retry)
4. ✅ Stale Cache Detection (SHOULD) - Tracks last_cache_update, reports in health response
### Known Issue
- SSH key `git_echo_id_ed25519` was rejected by Gitea on port 2222 - pushed via HTTPS + API token instead
- Root cause: Key fingerprint SHA256:W1BK9fCA53/or7iJkONbFSf3KJ6+oiAggPgisZNPhsc not registered in git-echo Gitea account
- Needs investigation: SSH key may need re-registration in Gitea