From 468528de668fa7e54de2279f8912e91a50c4e37b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 14 Jan 2026 16:25:02 -0600 Subject: [PATCH] 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 --- CLAUDE.md | 8 ++++++ src/main.rs | 5 ++++ src/version.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 src/version.rs diff --git a/CLAUDE.md b/CLAUDE.md index fef0924..35522de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,6 +49,13 @@ Lightweight Rust agent for remote SNMP polling. Deployed on customer networks to - Environment variable support - Logging with tracing - 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 - [x] **Protobuf Definitions** (`proto/agent.proto`) @@ -246,6 +253,7 @@ towerops-agent/ │ ├── main.rs # Entry point, CLI, initialization │ ├── config.rs # Types matching API responses │ ├── api_client.rs # HTTP client for Towerops API +│ ├── version.rs # Docker image version checking │ ├── metrics/ │ │ └── mod.rs # Metric types (SensorReading, InterfaceStat) │ ├── snmp/ diff --git a/src/main.rs b/src/main.rs index de1acee..ebba3ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod metrics; mod poller; mod proto; mod snmp; +mod version; use clap::Parser; use log::{info, LevelFilter, Metadata, Record}; @@ -71,6 +72,10 @@ async fn main() { let args = Args::parse(); info!("Towerops agent starting"); + + // Check for newer Docker image version + version::check_for_updates(); + info!("API URL: {}", args.api_url); info!( "Config refresh interval: {} seconds", diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..765b16f --- /dev/null +++ b/src/version.rs @@ -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, +} + +#[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, Box> { + 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::().is_ok()) +}