Simplify auto-update logic to always pull latest tag
The previous version checking didn't work because: - GitLab CI only creates semver tags when pushing Git tags - Most builds use SHA hash tags, not version tags - Comparing versions was unreliable New approach: - Simply pulls latest tag every hour - Checks docker pull output to see if image changed - Only restarts if a new image was actually pulled - Logs the last_updated timestamp from Docker Hub for visibility
This commit is contained in:
parent
d00e3782c3
commit
38c3451266
1 changed files with 39 additions and 104 deletions
143
src/version.rs
143
src/version.rs
|
|
@ -5,138 +5,73 @@ use std::process::Command;
|
||||||
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
|
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
|
||||||
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct DockerHubResponse {
|
|
||||||
results: Vec<DockerHubTag>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct DockerHubTag {
|
struct DockerHubTag {
|
||||||
name: String,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
last_updated: String,
|
last_updated: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
/// Simple startup check - just logs current version and that updates are automatic
|
||||||
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() {
|
pub fn check_for_updates() {
|
||||||
info!("Current version: {}", CURRENT_VERSION);
|
info!("Current version: {}", CURRENT_VERSION);
|
||||||
info!("Checking for newer Docker image versions...");
|
info!("Automatic updates enabled - will check every hour");
|
||||||
|
}
|
||||||
|
|
||||||
match get_update_info() {
|
/// Perform self-update by pulling latest image and exiting
|
||||||
Ok(info) => {
|
/// This always pulls latest since we use the :latest tag and can't reliably compare versions
|
||||||
if info.update_available {
|
/// Returns Ok(true) if update was initiated, Ok(false) if pull showed no changes
|
||||||
if let Some(ref latest) = info.latest_version {
|
pub fn perform_self_update() -> Result<bool, String> {
|
||||||
warn!(
|
// Check if latest tag exists on Docker Hub (quick sanity check)
|
||||||
"⚠️ Newer version available: {} (current: {})",
|
match check_latest_exists() {
|
||||||
latest, info.current_version
|
Ok(last_updated) => {
|
||||||
);
|
info!(
|
||||||
warn!(" Update with: docker pull {}:latest", DOCKER_IMAGE);
|
"Latest image on Docker Hub was updated at: {}",
|
||||||
}
|
last_updated
|
||||||
} else {
|
);
|
||||||
info!("✓ Running latest version ({})", info.current_version);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to check for updates: {}", e);
|
warn!("Could not verify latest tag on Docker Hub: {}", e);
|
||||||
|
// Continue anyway - if pull fails we'll catch it below
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Get update information without logging
|
// Pull the latest image
|
||||||
pub fn get_update_info() -> Result<UpdateInfo, String> {
|
info!("Pulling latest Docker image: {}:latest", DOCKER_IMAGE);
|
||||||
let latest_version = check_docker_hub().map_err(|e| e.to_string())?;
|
let output = Command::new("docker")
|
||||||
let update_available = if let Some(ref latest) = latest_version {
|
.args(["pull", &format!("{}:latest", DOCKER_IMAGE)])
|
||||||
latest != CURRENT_VERSION
|
.output()
|
||||||
} else {
|
.map_err(|e| format!("Failed to execute docker command: {}", e))?;
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(UpdateInfo {
|
if !output.status.success() {
|
||||||
current_version: CURRENT_VERSION.to_string(),
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
latest_version,
|
return Err(format!("Failed to pull image: {}", stderr));
|
||||||
update_available,
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform self-update by pulling new image and exiting
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
/// 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 {
|
// Check if the image was actually updated
|
||||||
info!("Already running latest version, no update needed");
|
if stdout.contains("Image is up to date") || stdout.contains("Already exists") {
|
||||||
|
info!("Image is already up to date, no restart needed");
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref latest) = info.latest_version {
|
info!("Successfully pulled new image");
|
||||||
warn!(
|
info!("Exiting to allow restart with new version...");
|
||||||
"Performing self-update: {} -> {}",
|
|
||||||
info.current_version, latest
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pull the new image
|
// Exit with success code - orchestrator (docker-compose/k8s) will restart with new image
|
||||||
info!("Pulling new Docker image: {}:latest", DOCKER_IMAGE);
|
std::process::exit(0);
|
||||||
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>> {
|
/// Check if the latest tag exists on Docker Hub and return its last_updated timestamp
|
||||||
|
fn check_latest_exists() -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://hub.docker.com/v2/repositories/{}/tags?page_size=10",
|
"https://hub.docker.com/v2/repositories/{}/tags/latest",
|
||||||
DOCKER_IMAGE
|
DOCKER_IMAGE
|
||||||
);
|
);
|
||||||
|
|
||||||
let response: DockerHubResponse = ureq::get(&url)
|
let response: DockerHubTag = ureq::get(&url)
|
||||||
.timeout(std::time::Duration::from_secs(10))
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
.call()?
|
.call()?
|
||||||
.into_json()?;
|
.into_json()?;
|
||||||
|
|
||||||
// Look for version tags (e.g., "0.1.0", "0.2.0")
|
Ok(response.last_updated)
|
||||||
// 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())
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue