Add automatic self-update capability to agent
The agent now checks for updates every hour and automatically updates itself when a new version is available on Docker Hub. Features: - Periodic update checks (hourly via scheduler) - Automatic Docker image pull when update available - Graceful exit and restart with new version - Non-blocking, runs in background task - Requires Docker socket mount for self-update Changes: - Add UpdateInfo struct and get_update_info() function - Add perform_self_update() to pull new image and restart - Add update check ticker to scheduler (hourly) - Include docker-cli in Dockerfile runtime stage - Update docker-compose.example.yml with socket mount - Update README and CLAUDE.md with auto-update docs The agent will log update status: - "Already running latest version" - no action - "Performing self-update: X -> Y" - pulling new image - "Exiting to allow restart with new version" - restarting Requires: - Docker socket mounted: /var/run/docker.sock:/var/run/docker.sock - restart: unless-stopped in docker-compose (to restart after exit)
This commit is contained in:
parent
3622cbf0ee
commit
824f4388eb
6 changed files with 117 additions and 14 deletions
|
|
@ -37,6 +37,7 @@ Lightweight Rust agent for remote SNMP polling. Deployed on customer networks to
|
|||
- Heartbeat every 60 seconds
|
||||
- Cleanup every hour
|
||||
- Poll check every 5 seconds
|
||||
- Update check and auto-update every hour
|
||||
|
||||
- [x] **Executor** (`poller/executor.rs`)
|
||||
- poll_sensors() - Poll configured sensors
|
||||
|
|
@ -51,11 +52,14 @@ Lightweight Rust agent for remote SNMP polling. Deployed on customer networks to
|
|||
- Graceful startup/shutdown
|
||||
- Docker image version checking on startup
|
||||
|
||||
- [x] **Version Checking** (`version.rs`)
|
||||
- [x] **Version Checking & Auto-Update** (`version.rs`)
|
||||
- Checks Docker Hub for newer image versions on startup
|
||||
- Performs periodic checks every hour (configurable in scheduler)
|
||||
- Automatically pulls new image and restarts when update available
|
||||
- Compares current version with latest available
|
||||
- Logs warnings if updates are available
|
||||
- Non-blocking, fails gracefully if Docker Hub unavailable
|
||||
- Requires Docker socket mount (`/var/run/docker.sock`) for self-update
|
||||
|
||||
### Protocol Buffers Integration
|
||||
- [x] **Protobuf Definitions** (`proto/agent.proto`)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ RUN RUST_TARGET=$(cat /tmp/rust-target) && \
|
|||
FROM alpine:3.19
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache ca-certificates su-exec
|
||||
# docker-cli is needed for self-update functionality
|
||||
RUN apk add --no-cache ca-certificates su-exec docker-cli
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /data
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ The Towerops agent enables customers to deploy SNMP polling infrastructure on th
|
|||
- **Secure Communication**: All communication with Towerops API uses HTTPS with token authentication
|
||||
- **Metric Buffering**: Stores up to 24 hours of metrics in SQLite when API is unavailable
|
||||
- **Automatic Configuration**: Fetches polling configuration from the Towerops API
|
||||
- **Automatic Updates**: Checks for new versions hourly and self-updates when available (requires Docker socket access)
|
||||
- **Low Resource Usage**: Built in Rust for minimal memory and CPU footprint (< 256 MB RAM typical)
|
||||
- **Docker Ready**: Pre-built Docker images for easy deployment
|
||||
|
||||
|
|
@ -40,6 +41,8 @@ services:
|
|||
- TOWEROPS_AGENT_TOKEN=your-agent-token-here
|
||||
volumes:
|
||||
- ./data:/data
|
||||
# Required for automatic self-updates
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
```
|
||||
|
||||
2. Start the agent:
|
||||
|
|
@ -48,6 +51,8 @@ services:
|
|||
docker-compose up -d
|
||||
```
|
||||
|
||||
**Note**: The Docker socket mount (`/var/run/docker.sock`) is required for automatic updates. The agent checks for new versions hourly and will automatically pull and restart with the latest version when available.
|
||||
|
||||
### Configuration
|
||||
|
||||
The agent is configured via environment variables:
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ services:
|
|||
# Persistent storage for metrics buffering
|
||||
- ./data:/data
|
||||
|
||||
# Docker socket (required for automatic self-updates)
|
||||
# The agent will check for updates hourly and automatically update itself
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# Optional: Resource limits
|
||||
deploy:
|
||||
resources:
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ impl Scheduler {
|
|||
let mut heartbeat_ticker = interval(Duration::from_secs(60));
|
||||
let mut cleanup_ticker = interval(Duration::from_secs(3600)); // Cleanup every hour
|
||||
let mut poll_ticker = interval(Duration::from_secs(5)); // Check if polling needed every 5s
|
||||
let mut update_ticker = interval(Duration::from_secs(3600)); // Check for updates every hour
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
|
|
@ -133,6 +134,10 @@ impl Scheduler {
|
|||
error!("Polling error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
_ = update_ticker.tick() => {
|
||||
self.check_and_update().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -250,4 +255,27 @@ impl Scheduler {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_and_update(&self) {
|
||||
info!("Checking for agent updates");
|
||||
|
||||
// Run version check in blocking thread to avoid blocking event loop
|
||||
let result = tokio::task::spawn_blocking(crate::version::perform_self_update).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(true)) => {
|
||||
info!("Update initiated, container will restart with new version");
|
||||
// perform_self_update calls std::process::exit(0), so we won't reach here
|
||||
}
|
||||
Ok(Ok(false)) => {
|
||||
info!("Already running latest version");
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!("Failed to perform self-update: {}", e);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Update check task failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use log::{info, warn};
|
||||
use serde::Deserialize;
|
||||
use std::process::Command;
|
||||
|
||||
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
|
||||
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
|
@ -16,32 +17,92 @@ struct DockerHubTag {
|
|||
last_updated: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct UpdateInfo {
|
||||
pub current_version: String,
|
||||
pub latest_version: Option<String>,
|
||||
pub update_available: bool,
|
||||
}
|
||||
|
||||
/// Check if a newer version of the Docker image is available
|
||||
pub fn check_for_updates() {
|
||||
info!("Current version: {}", CURRENT_VERSION);
|
||||
info!("Checking for newer Docker image versions...");
|
||||
|
||||
match check_docker_hub() {
|
||||
Ok(Some(latest_version)) => {
|
||||
if latest_version != CURRENT_VERSION {
|
||||
warn!(
|
||||
"⚠️ Newer version available: {} (current: {})",
|
||||
latest_version, CURRENT_VERSION
|
||||
);
|
||||
warn!(" Update with: docker pull {}:latest", DOCKER_IMAGE);
|
||||
match get_update_info() {
|
||||
Ok(info) => {
|
||||
if info.update_available {
|
||||
if let Some(ref latest) = info.latest_version {
|
||||
warn!(
|
||||
"⚠️ Newer version available: {} (current: {})",
|
||||
latest, info.current_version
|
||||
);
|
||||
warn!(" Update with: docker pull {}:latest", DOCKER_IMAGE);
|
||||
}
|
||||
} else {
|
||||
info!("✓ Running latest version ({})", CURRENT_VERSION);
|
||||
info!("✓ Running latest version ({})", info.current_version);
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("Unable to determine latest version from Docker Hub");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to check for updates: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get update information without logging
|
||||
pub fn get_update_info() -> Result<UpdateInfo, String> {
|
||||
let latest_version = check_docker_hub().map_err(|e| e.to_string())?;
|
||||
let update_available = if let Some(ref latest) = latest_version {
|
||||
latest != CURRENT_VERSION
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(UpdateInfo {
|
||||
current_version: CURRENT_VERSION.to_string(),
|
||||
latest_version,
|
||||
update_available,
|
||||
})
|
||||
}
|
||||
|
||||
/// Perform self-update by pulling new image and exiting
|
||||
/// Returns Ok(true) if update was initiated, Ok(false) if already up-to-date
|
||||
pub fn perform_self_update() -> Result<bool, String> {
|
||||
let info = get_update_info()?;
|
||||
|
||||
if !info.update_available {
|
||||
info!("Already running latest version, no update needed");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(ref latest) = info.latest_version {
|
||||
warn!(
|
||||
"Performing self-update: {} -> {}",
|
||||
info.current_version, latest
|
||||
);
|
||||
|
||||
// Pull the new image
|
||||
info!("Pulling new Docker image: {}:latest", DOCKER_IMAGE);
|
||||
let output = Command::new("docker")
|
||||
.args(["pull", &format!("{}:latest", DOCKER_IMAGE)])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute docker command: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("Failed to pull image: {}", stderr));
|
||||
}
|
||||
|
||||
info!("Successfully pulled new image");
|
||||
info!("Exiting to allow restart with new version...");
|
||||
|
||||
// Exit with success code - orchestrator (docker-compose/k8s) will restart with new image
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn check_docker_hub() -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
let url = format!(
|
||||
"https://hub.docker.com/v2/repositories/{}/tags?page_size=10",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue