Add Docker image version checking on agent startup

The agent now checks Docker Hub on startup to see if a newer version
is available and logs a warning if an update is detected.

Changes:
- Add version.rs module to query Docker Hub API
- Check for newer versions on startup (non-blocking)
- Log warning with docker pull command if update available
- Gracefully handle Docker Hub API failures
- Update CLAUDE.md with version checking documentation
This commit is contained in:
Graham McIntire 2026-01-14 16:25:02 -06:00
parent 37b3e1c8be
commit 468528de66
No known key found for this signature in database
3 changed files with 91 additions and 0 deletions

View file

@ -49,6 +49,13 @@ Lightweight Rust agent for remote SNMP polling. Deployed on customer networks to
- Environment variable support - Environment variable support
- Logging with tracing - Logging with tracing
- Graceful startup/shutdown - Graceful startup/shutdown
- Docker image version checking on startup
- [x] **Version Checking** (`version.rs`)
- Checks Docker Hub for newer image versions on startup
- Compares current version with latest available
- Logs warnings if updates are available
- Non-blocking, fails gracefully if Docker Hub unavailable
### Protocol Buffers Integration ### Protocol Buffers Integration
- [x] **Protobuf Definitions** (`proto/agent.proto`) - [x] **Protobuf Definitions** (`proto/agent.proto`)
@ -246,6 +253,7 @@ towerops-agent/
│ ├── main.rs # Entry point, CLI, initialization │ ├── main.rs # Entry point, CLI, initialization
│ ├── config.rs # Types matching API responses │ ├── config.rs # Types matching API responses
│ ├── api_client.rs # HTTP client for Towerops API │ ├── api_client.rs # HTTP client for Towerops API
│ ├── version.rs # Docker image version checking
│ ├── metrics/ │ ├── metrics/
│ │ └── mod.rs # Metric types (SensorReading, InterfaceStat) │ │ └── mod.rs # Metric types (SensorReading, InterfaceStat)
│ ├── snmp/ │ ├── snmp/

View file

@ -5,6 +5,7 @@ mod metrics;
mod poller; mod poller;
mod proto; mod proto;
mod snmp; mod snmp;
mod version;
use clap::Parser; use clap::Parser;
use log::{info, LevelFilter, Metadata, Record}; use log::{info, LevelFilter, Metadata, Record};
@ -71,6 +72,10 @@ async fn main() {
let args = Args::parse(); let args = Args::parse();
info!("Towerops agent starting"); info!("Towerops agent starting");
// Check for newer Docker image version
version::check_for_updates();
info!("API URL: {}", args.api_url); info!("API URL: {}", args.api_url);
info!( info!(
"Config refresh interval: {} seconds", "Config refresh interval: {} seconds",

78
src/version.rs Normal file
View file

@ -0,0 +1,78 @@
use log::{info, warn};
use serde::Deserialize;
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Deserialize)]
struct DockerHubResponse {
results: Vec<DockerHubTag>,
}
#[derive(Debug, Deserialize)]
struct DockerHubTag {
name: String,
#[allow(dead_code)]
last_updated: String,
}
/// 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);
} else {
info!("✓ Running latest version ({})", CURRENT_VERSION);
}
}
Ok(None) => {
info!("Unable to determine latest version from Docker Hub");
}
Err(e) => {
warn!("Failed to check for updates: {}", e);
}
}
}
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",
DOCKER_IMAGE
);
let response: DockerHubResponse = ureq::get(&url)
.timeout(std::time::Duration::from_secs(10))
.call()?
.into_json()?;
// Look for version tags (e.g., "0.1.0", "0.2.0")
// Ignore "latest" tag as it doesn't tell us the actual version
let version_tags: Vec<&str> = response
.results
.iter()
.map(|t| t.name.as_str())
.filter(|name| !name.eq_ignore_ascii_case("latest"))
.filter(|name| is_semver(name))
.collect();
if let Some(latest) = version_tags.first() {
Ok(Some(latest.to_string()))
} else {
// If no version tags found, try to get info from "latest" tag
// but we can't determine actual version number
Ok(None)
}
}
fn is_semver(s: &str) -> bool {
// Simple check: version string should match pattern like "0.1.0" or "v0.1.0"
let s = s.strip_prefix('v').unwrap_or(s);
let parts: Vec<&str> = s.split('.').collect();
parts.len() == 3 && parts.iter().all(|p| p.parse::<u32>().is_ok())
}