refactor: remove 4 more dependencies (rand, ureq, tiny_http, anyhow)
Removed dependencies: - rand: Replaced with SystemTime-based random for ICMP ID/sequence - ureq: Removed Docker Hub version checking (Watchtower handles updates) - tiny_http: Replaced with raw TCP listener for /health endpoint - anyhow: Replaced with std::error::Error trait Benefits: - Binary size: 2.3M → 1.5M (800KB reduction, 35% smaller!) - Dependencies: 19 → 11 direct dependencies (-8 total) - Tests: 80 → 67 (removed Docker Hub API tests) - All tests passing Custom implementations: - ICMP random: Use nanosecond timestamp for ID/sequence - Health endpoint: Simple HTTP/1.1 parser on raw TCP - Error handling: type Result<T> = std::result::Result<T, Box<dyn Error>> Total progress from initial state: - Binary: 2.4M → 1.5M (37.5% reduction!) - Dependencies removed: 8 (thiserror, hostname, chrono, base64, rand, ureq, tiny_http, anyhow)
This commit is contained in:
parent
d02193921d
commit
55fb691440
6 changed files with 86 additions and 448 deletions
126
Cargo.lock
generated
126
Cargo.lock
generated
|
|
@ -2,12 +2,6 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
|
|
@ -73,18 +67,6 @@ version = "1.0.100"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
|
|
@ -128,12 +110,6 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chunked_transfer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.54"
|
||||
|
|
@ -189,15 +165,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
|
|
@ -275,16 +242,6 @@ version = "0.5.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
|
|
@ -444,12 +401,6 @@ version = "1.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
|
|
@ -613,15 +564,6 @@ version = "2.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
|
|
@ -878,22 +820,7 @@ dependencies = [
|
|||
"log",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.102.8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.8",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
|
@ -918,17 +845,6 @@ dependencies = [
|
|||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
|
|
@ -1100,18 +1016,6 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny_http"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
|
||||
dependencies = [
|
||||
"ascii",
|
||||
"chunked_transfer",
|
||||
"httpdate",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
|
|
@ -1154,7 +1058,7 @@ version = "0.25.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
|
||||
dependencies = [
|
||||
"rustls 0.22.4",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
|
@ -1167,7 +1071,7 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
|||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls 0.22.4",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
|
|
@ -1179,22 +1083,18 @@ dependencies = [
|
|||
name = "towerops-agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"futures",
|
||||
"log",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snmp",
|
||||
"socket2 0.5.10",
|
||||
"tiny_http",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1210,7 +1110,7 @@ dependencies = [
|
|||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"rustls 0.22.4",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
|
|
@ -1236,24 +1136,6 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"flate2",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.36",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
snmp = "0.2"
|
||||
ureq = { version = "2.10", features = ["json"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net"] }
|
||||
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
|
||||
futures = "0.3"
|
||||
|
|
@ -15,10 +14,7 @@ log = { version = "0.4", features = ["std"] }
|
|||
clap = { version = "4.0", features = ["derive", "env"] }
|
||||
prost = "0.13"
|
||||
prost-types = "0.13"
|
||||
tiny_http = "0.12"
|
||||
anyhow = "1.0"
|
||||
socket2 = "0.5"
|
||||
rand = "0.8"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tiny_http::{Response, Server};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealthStatus {
|
||||
|
|
@ -12,54 +14,64 @@ pub struct HealthStatus {
|
|||
pub uptime_seconds: u64,
|
||||
}
|
||||
|
||||
/// Start a simple health check HTTP server
|
||||
/// Start a simple health check HTTP server using raw TCP
|
||||
pub async fn start_health_server(port: u16) -> Result<()> {
|
||||
let start_time = Arc::new(Instant::now());
|
||||
let addr = format!("0.0.0.0:{}", port);
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let addr = format!("0.0.0.0:{}", port);
|
||||
info!("Starting health endpoint on {}", addr);
|
||||
info!("Starting health endpoint on {}", addr);
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
|
||||
let server = Server::http(&addr)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to start health server: {}", e))?;
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((mut socket, _)) => {
|
||||
let start_time = Arc::clone(&start_time);
|
||||
|
||||
for request in server.incoming_requests() {
|
||||
let path = request.url();
|
||||
tokio::spawn(async move {
|
||||
let mut buffer = [0u8; 1024];
|
||||
|
||||
if path == "/health" {
|
||||
let uptime = start_time.elapsed().as_secs();
|
||||
let status = HealthStatus {
|
||||
status: "healthy".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
uptime_seconds: uptime,
|
||||
};
|
||||
// Read the HTTP request
|
||||
if let Ok(n) = socket.read(&mut buffer).await {
|
||||
if n > 0 {
|
||||
let request = String::from_utf8_lossy(&buffer[..n]);
|
||||
|
||||
let json = serde_json::to_string(&status).unwrap_or_else(|_| {
|
||||
r#"{"status":"error","message":"Failed to serialize health status"}"#
|
||||
.to_string()
|
||||
// Check if this is a GET request to /health
|
||||
if request.starts_with("GET /health") {
|
||||
let uptime = start_time.elapsed().as_secs();
|
||||
let status = HealthStatus {
|
||||
status: "healthy".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
uptime_seconds: uptime,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&status).unwrap_or_else(|_| {
|
||||
r#"{"status":"error","message":"Failed to serialize health status"}"#
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
|
||||
json.len(),
|
||||
json
|
||||
);
|
||||
|
||||
let _ = socket.write_all(response.as_bytes()).await;
|
||||
} else {
|
||||
// 404 for other paths
|
||||
let response = "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot found";
|
||||
let _ = socket.write_all(response.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = socket.shutdown().await;
|
||||
});
|
||||
|
||||
let response = Response::from_string(json)
|
||||
.with_header(
|
||||
tiny_http::Header::from_bytes(
|
||||
&b"Content-Type"[..],
|
||||
&b"application/json"[..],
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.with_status_code(200);
|
||||
|
||||
let _ = request.respond(response);
|
||||
} else {
|
||||
let _ = request.respond(Response::from_string("Not found").with_status_code(404));
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to accept health check connection: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
31
src/ping.rs
31
src/ping.rs
|
|
@ -1,9 +1,10 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::time::Duration;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::timeout;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// ICMP message types
|
||||
const ICMP_ECHO: u8 = 8;
|
||||
const ICMP_ECHOREPLY: u8 = 0;
|
||||
|
|
@ -33,7 +34,7 @@ pub async fn ping(ip: IpAddr, timeout_duration: Duration) -> Result<Duration> {
|
|||
match ip {
|
||||
IpAddr::V4(ipv4) => ping_ipv4(ipv4, timeout_duration).await?,
|
||||
IpAddr::V6(_) => {
|
||||
return Err(anyhow!("IPv6 ping not yet implemented"));
|
||||
return Err("IPv6 ping not yet implemented".into());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,9 +42,13 @@ pub async fn ping(ip: IpAddr, timeout_duration: Duration) -> Result<Duration> {
|
|||
}
|
||||
|
||||
async fn ping_ipv4(ip: Ipv4Addr, timeout_duration: Duration) -> Result<()> {
|
||||
// Generate unique identifier and sequence number
|
||||
let identifier = rand::random::<u16>();
|
||||
let sequence = rand::random::<u16>();
|
||||
// Generate unique identifier and sequence number using timestamp
|
||||
// This provides sufficient randomness for ICMP ping identification
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
let identifier = (now.as_nanos() & 0xFFFF) as u16;
|
||||
let sequence = ((now.as_nanos() >> 16) & 0xFFFF) as u16;
|
||||
|
||||
// Build ICMP echo request packet
|
||||
let packet = build_icmp_echo_request(identifier, sequence);
|
||||
|
|
@ -74,8 +79,10 @@ async fn ping_ipv4(ip: Ipv4Addr, timeout_duration: Duration) -> Result<()> {
|
|||
let mut buf = [0u8; 1024];
|
||||
let n = timeout(timeout_duration, socket.recv(&mut buf))
|
||||
.await
|
||||
.map_err(|_| anyhow!("Ping timeout"))?
|
||||
.map_err(|e| anyhow!("Failed to receive ping reply: {}", e))?;
|
||||
.map_err(|_| -> Box<dyn std::error::Error + Send + Sync> { "Ping timeout".into() })?
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to receive ping reply: {}", e).into()
|
||||
})?;
|
||||
|
||||
// Parse and validate the reply
|
||||
parse_icmp_reply(&buf[..n], identifier, sequence)?;
|
||||
|
|
@ -163,16 +170,16 @@ fn parse_icmp_reply(packet: &[u8], expected_identifier: u16, expected_sequence:
|
|||
format!("len={} (too short)", packet.len())
|
||||
};
|
||||
|
||||
Err(anyhow!(
|
||||
Err(format!(
|
||||
"Invalid ICMP reply packet (expected seq={}): {}",
|
||||
expected_sequence,
|
||||
packet_preview
|
||||
))
|
||||
).into())
|
||||
}
|
||||
|
||||
fn try_parse_icmp(packet: &[u8], _expected_identifier: u16, expected_sequence: u16) -> Result<()> {
|
||||
if packet.len() < 8 {
|
||||
return Err(anyhow!("Packet too short"));
|
||||
return Err("Packet too short".into());
|
||||
}
|
||||
|
||||
let icmp_type = packet[0];
|
||||
|
|
@ -186,13 +193,13 @@ fn try_parse_icmp(packet: &[u8], _expected_identifier: u16, expected_sequence: u
|
|||
if icmp_type == ICMP_ECHOREPLY && icmp_code == 0 && sequence == expected_sequence {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
Err(format!(
|
||||
"ICMP packet mismatch (type={}, code={}, id={}, seq={})",
|
||||
icmp_type,
|
||||
icmp_code,
|
||||
identifier,
|
||||
sequence
|
||||
))
|
||||
).into())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
270
src/version.rs
270
src/version.rs
|
|
@ -1,133 +1,15 @@
|
|||
use log::{info, warn};
|
||||
use serde::Deserialize;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
const DOCKER_IMAGE: &str = "gmcintire/towerops-agent";
|
||||
use log::info;
|
||||
|
||||
// Get version at runtime - prefers BUILD_VERSION from build.rs, falls back to Cargo.toml
|
||||
fn current_version() -> &'static str {
|
||||
option_env!("BUILD_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DockerHubResponse {
|
||||
results: Vec<DockerHubTag>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DockerHubTag {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct Version {
|
||||
major: u32,
|
||||
minor: u32,
|
||||
patch: u32,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
let s = s.strip_prefix('v').unwrap_or(s);
|
||||
let parts: Vec<&str> = s.split('.').collect();
|
||||
|
||||
if parts.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Version {
|
||||
major: parts[0].parse().ok()?,
|
||||
minor: parts[1].parse().ok()?,
|
||||
patch: parts[2].parse().ok()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Version {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Version {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.major.cmp(&other.major) {
|
||||
Ordering::Equal => match self.minor.cmp(&other.minor) {
|
||||
Ordering::Equal => self.patch.cmp(&other.patch),
|
||||
other => other,
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Startup check - logs current version and checks for updates
|
||||
/// Startup check - logs current version
|
||||
pub fn check_for_updates() {
|
||||
let current_ver = current_version();
|
||||
info!("Current version: {}", current_ver);
|
||||
|
||||
match get_latest_version() {
|
||||
Ok(latest) => {
|
||||
let current = Version::parse(current_ver);
|
||||
let latest_version = Version::parse(&latest);
|
||||
|
||||
match (current, latest_version) {
|
||||
(Some(curr), Some(lat)) => {
|
||||
if lat > curr {
|
||||
warn!(
|
||||
"⚠️ Newer version available: {} (current: {})",
|
||||
latest, current_ver
|
||||
);
|
||||
warn!(" Automatic updates will pull new version every hour");
|
||||
} else {
|
||||
info!("✓ Running latest version ({})", current_ver);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
info!("Could not compare versions, automatic updates enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not check for updates: {}", e);
|
||||
info!("Automatic updates enabled - will check every hour");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the latest version from Docker Hub
|
||||
fn get_latest_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let url = format!(
|
||||
"https://hub.docker.com/v2/repositories/{}/tags?page_size=100",
|
||||
DOCKER_IMAGE
|
||||
);
|
||||
|
||||
let response: DockerHubResponse = ureq::get(&url)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.call()?
|
||||
.into_json()?;
|
||||
|
||||
extract_latest_version_from_response(response)
|
||||
}
|
||||
|
||||
/// Extract the latest version from Docker Hub response
|
||||
fn extract_latest_version_from_response(
|
||||
response: DockerHubResponse,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Filter for semver tags and find the latest
|
||||
let mut versions: Vec<Version> = response
|
||||
.results
|
||||
.iter()
|
||||
.filter_map(|tag| Version::parse(&tag.name))
|
||||
.collect();
|
||||
|
||||
versions.sort();
|
||||
versions.reverse(); // Highest version first
|
||||
|
||||
versions
|
||||
.first()
|
||||
.map(|v| format!("{}.{}.{}", v.major, v.minor, v.patch))
|
||||
.ok_or_else(|| "No valid semver tags found".into())
|
||||
info!("Watchtower will automatically update to new versions");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -142,150 +24,6 @@ mod tests {
|
|||
// Just verify it's a non-empty string
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_parse_valid() {
|
||||
let v = Version::parse("1.2.3").unwrap();
|
||||
assert_eq!(v.major, 1);
|
||||
assert_eq!(v.minor, 2);
|
||||
assert_eq!(v.patch, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_parse_with_v_prefix() {
|
||||
let v = Version::parse("v2.5.8").unwrap();
|
||||
assert_eq!(v.major, 2);
|
||||
assert_eq!(v.minor, 5);
|
||||
assert_eq!(v.patch, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_parse_invalid() {
|
||||
assert!(Version::parse("1.2").is_none());
|
||||
assert!(Version::parse("1.2.3.4").is_none());
|
||||
assert!(Version::parse("a.b.c").is_none());
|
||||
assert!(Version::parse("1.2.x").is_none());
|
||||
assert!(Version::parse("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_comparison_major() {
|
||||
let v1 = Version::parse("2.0.0").unwrap();
|
||||
let v2 = Version::parse("1.9.9").unwrap();
|
||||
assert!(v1 > v2);
|
||||
assert!(v2 < v1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_comparison_minor() {
|
||||
let v1 = Version::parse("1.5.0").unwrap();
|
||||
let v2 = Version::parse("1.4.9").unwrap();
|
||||
assert!(v1 > v2);
|
||||
assert!(v2 < v1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_comparison_patch() {
|
||||
let v1 = Version::parse("1.2.4").unwrap();
|
||||
let v2 = Version::parse("1.2.3").unwrap();
|
||||
assert!(v1 > v2);
|
||||
assert!(v2 < v1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_comparison_equal() {
|
||||
let v1 = Version::parse("1.2.3").unwrap();
|
||||
let v2 = Version::parse("1.2.3").unwrap();
|
||||
assert_eq!(v1, v2);
|
||||
assert!(!(v1 > v2));
|
||||
assert!(!(v1 < v2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_sorting() {
|
||||
let mut versions = vec![
|
||||
Version::parse("1.2.3").unwrap(),
|
||||
Version::parse("2.0.0").unwrap(),
|
||||
Version::parse("1.5.0").unwrap(),
|
||||
Version::parse("1.2.10").unwrap(),
|
||||
];
|
||||
versions.sort();
|
||||
assert_eq!(versions[0], Version::parse("1.2.3").unwrap());
|
||||
assert_eq!(versions[1], Version::parse("1.2.10").unwrap());
|
||||
assert_eq!(versions[2], Version::parse("1.5.0").unwrap());
|
||||
assert_eq!(versions[3], Version::parse("2.0.0").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dockerhub_response_deserialize() {
|
||||
let json = r#"{"results":[{"name":"v1.0.0"},{"name":"v1.0.1"}]}"#;
|
||||
let response: DockerHubResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(response.results.len(), 2);
|
||||
assert_eq!(response.results[0].name, "v1.0.0");
|
||||
assert_eq!(response.results[1].name, "v1.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_latest_version_from_response() {
|
||||
let response = DockerHubResponse {
|
||||
results: vec![
|
||||
DockerHubTag {
|
||||
name: "v1.0.0".to_string(),
|
||||
},
|
||||
DockerHubTag {
|
||||
name: "v1.2.0".to_string(),
|
||||
},
|
||||
DockerHubTag {
|
||||
name: "v1.1.5".to_string(),
|
||||
},
|
||||
DockerHubTag {
|
||||
name: "latest".to_string(),
|
||||
}, // Should be ignored
|
||||
],
|
||||
};
|
||||
|
||||
let latest = extract_latest_version_from_response(response).unwrap();
|
||||
assert_eq!(latest, "1.2.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_latest_version_no_valid_tags() {
|
||||
let response = DockerHubResponse {
|
||||
results: vec![
|
||||
DockerHubTag {
|
||||
name: "latest".to_string(),
|
||||
},
|
||||
DockerHubTag {
|
||||
name: "main".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let result = extract_latest_version_from_response(response);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("No valid semver"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_latest_version_empty() {
|
||||
let response = DockerHubResponse { results: vec![] };
|
||||
|
||||
let result = extract_latest_version_from_response(response);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_debug() {
|
||||
let v = Version {
|
||||
major: 1,
|
||||
minor: 2,
|
||||
patch: 3,
|
||||
};
|
||||
let debug_str = format!("{:?}", v);
|
||||
assert!(debug_str.contains("1"));
|
||||
assert!(debug_str.contains("2"));
|
||||
assert!(debug_str.contains("3"));
|
||||
}
|
||||
|
||||
// Note: check_for_updates() and get_latest_version() are tested via integration tests
|
||||
// as they require network access to Docker Hub
|
||||
// Note: check_for_updates() just logs the version, no testing needed
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
///
|
||||
/// Connection URL: {url}/socket/agent/websocket
|
||||
/// Authentication: Token sent in Phoenix channel join payload
|
||||
use anyhow::{Context, Result};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use prost::Message;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -17,6 +16,8 @@ use tokio_tungstenite::{
|
|||
connect_async, tungstenite::protocol::Message as WsMessage, MaybeTlsStream, WebSocketStream,
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
use crate::ping::ping;
|
||||
use crate::proto::agent::{
|
||||
AgentHeartbeat, AgentJob, AgentJobList, JobType, MonitoringCheck, QueryType, SnmpResult,
|
||||
|
|
@ -66,7 +67,7 @@ impl AgentClient {
|
|||
log::error!("WebSocket connection failed: {}", e);
|
||||
e
|
||||
})
|
||||
.context("Failed to connect to WebSocket")?;
|
||||
.map_err(|e| format!("Failed to connect to WebSocket: {}", e))?;
|
||||
|
||||
log::info!("Connected to Towerops server at {}", url);
|
||||
|
||||
|
|
@ -323,7 +324,9 @@ impl AgentClient {
|
|||
|
||||
/// Execute an SNMP job and collect results.
|
||||
async fn execute_job(job: AgentJob, result_tx: mpsc::UnboundedSender<SnmpResult>) -> Result<()> {
|
||||
let snmp_device = job.snmp_device.context("Job missing SNMP device info")?;
|
||||
let snmp_device = job
|
||||
.snmp_device
|
||||
.ok_or("Job missing SNMP device info")?;
|
||||
let mut oid_values: HashMap<String, String> = HashMap::new();
|
||||
let snmp_client = SnmpClient::new();
|
||||
|
||||
|
|
@ -468,7 +471,7 @@ fn base64_decode(encoded: &str) -> Result<Vec<u8>> {
|
|||
} else {
|
||||
let val = decode_map[byte as usize];
|
||||
if val == 0xFF {
|
||||
anyhow::bail!("Invalid base64 character");
|
||||
return Err("Invalid base64 character".into());
|
||||
}
|
||||
buf[i] = val;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue