cleanup
This commit is contained in:
parent
8ee809d21e
commit
cf08b0589b
5 changed files with 32 additions and 217 deletions
24
Dockerfile
24
Dockerfile
|
|
@ -46,30 +46,16 @@ RUN RUST_TARGET=$(cat /tmp/rust-target) && \
|
||||||
FROM alpine:3.19
|
FROM alpine:3.19
|
||||||
|
|
||||||
# Install runtime dependencies
|
# Install runtime dependencies
|
||||||
# docker-cli is needed for self-update functionality
|
|
||||||
# iputils provides ping with setuid root (doesn't require CAP_NET_RAW)
|
# iputils provides ping with setuid root (doesn't require CAP_NET_RAW)
|
||||||
RUN apk add --no-cache ca-certificates su-exec docker-cli iputils
|
RUN apk add --no-cache ca-certificates iputils
|
||||||
|
|
||||||
# Create data directory
|
|
||||||
RUN mkdir -p /data
|
|
||||||
|
|
||||||
# Copy binary from builder
|
# Copy binary from builder
|
||||||
COPY --from=builder /tmp/towerops-agent /usr/local/bin/towerops-agent
|
COPY --from=builder /tmp/towerops-agent /usr/local/bin/towerops-agent
|
||||||
|
|
||||||
# Copy entrypoint script
|
# Create non-root user
|
||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
||||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
|
||||||
|
|
||||||
# Volume for database
|
|
||||||
VOLUME ["/data"]
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
|
||||||
CMD test -f /data/towerops-agent.db || exit 1
|
|
||||||
|
|
||||||
# Create non-root user (entrypoint will drop to this user)
|
|
||||||
RUN addgroup -g 1000 towerops && \
|
RUN addgroup -g 1000 towerops && \
|
||||||
adduser -D -u 1000 -G towerops towerops
|
adduser -D -u 1000 -G towerops towerops
|
||||||
|
|
||||||
# Start as root, entrypoint will fix permissions and drop to towerops user
|
USER towerops
|
||||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
||||||
|
ENTRYPOINT ["towerops-agent"]
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Ensure /data directory exists and has proper permissions
|
|
||||||
if [ -d /data ]; then
|
|
||||||
# Fix ownership if needed
|
|
||||||
chown -R towerops:towerops /data
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If Docker socket is mounted, add towerops user to docker group
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
# Get the GID of the docker socket
|
|
||||||
DOCKER_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || stat -f '%g' /var/run/docker.sock 2>/dev/null)
|
|
||||||
|
|
||||||
# Create docker group with the same GID as host's docker socket
|
|
||||||
if ! getent group "$DOCKER_GID" >/dev/null 2>&1; then
|
|
||||||
addgroup -g "$DOCKER_GID" docker
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add towerops user to the docker group
|
|
||||||
addgroup towerops docker 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Drop to towerops user and run the agent
|
|
||||||
exec su-exec towerops towerops-agent "$@"
|
|
||||||
26
fly.toml
Normal file
26
fly.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# fly.toml app configuration file generated for towerops-agent on 2026-01-24T12:26:30-06:00
|
||||||
|
#
|
||||||
|
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||||
|
#
|
||||||
|
|
||||||
|
app = 'towerops-agent'
|
||||||
|
primary_region = 'dfw'
|
||||||
|
|
||||||
|
[build]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
RUST_LOG = 'info'
|
||||||
|
|
||||||
|
[http_service]
|
||||||
|
internal_port = 8080
|
||||||
|
force_https = true
|
||||||
|
auto_stop_machines = 'stop'
|
||||||
|
auto_start_machines = true
|
||||||
|
min_machines_running = 0
|
||||||
|
processes = ['app']
|
||||||
|
|
||||||
|
[[vm]]
|
||||||
|
memory = '256mb'
|
||||||
|
cpu_kind = 'shared'
|
||||||
|
cpus = 1
|
||||||
|
memory_mb = 256
|
||||||
163
src/health.rs
163
src/health.rs
|
|
@ -1,163 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Instant;
|
|
||||||
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 {
|
|
||||||
pub status: String,
|
|
||||||
pub version: String,
|
|
||||||
pub uptime_seconds: u64,
|
|
||||||
pub connected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start a simple health check HTTP server using raw TCP.
|
|
||||||
///
|
|
||||||
/// The health check returns:
|
|
||||||
/// - 200 OK with status "healthy" when WebSocket is connected
|
|
||||||
/// - 503 Service Unavailable with status "unhealthy" when WebSocket is disconnected
|
|
||||||
///
|
|
||||||
/// This allows Kubernetes readiness probes to remove the pod from service
|
|
||||||
/// when the WebSocket connection is broken.
|
|
||||||
pub async fn start_health_server(port: u16, connected: Arc<AtomicBool>) -> Result<()> {
|
|
||||||
let start_time = Arc::new(Instant::now());
|
|
||||||
let addr = format!("0.0.0.0:{}", port);
|
|
||||||
|
|
||||||
crate::log_info!("Starting health endpoint on {}", addr);
|
|
||||||
let listener = TcpListener::bind(&addr).await?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match listener.accept().await {
|
|
||||||
Ok((mut socket, _)) => {
|
|
||||||
let start_time = Arc::clone(&start_time);
|
|
||||||
let connected = Arc::clone(&connected);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut buffer = [0u8; 1024];
|
|
||||||
|
|
||||||
// Read the HTTP request
|
|
||||||
if let Ok(n) = socket.read(&mut buffer).await {
|
|
||||||
if n > 0 {
|
|
||||||
let request = String::from_utf8_lossy(&buffer[..n]);
|
|
||||||
|
|
||||||
// Check if this is a GET request to /health
|
|
||||||
if request.starts_with("GET /health") {
|
|
||||||
let is_connected = connected.load(Ordering::Relaxed);
|
|
||||||
let uptime = start_time.elapsed().as_secs();
|
|
||||||
|
|
||||||
let status = HealthStatus {
|
|
||||||
status: if is_connected {
|
|
||||||
"healthy".to_string()
|
|
||||||
} else {
|
|
||||||
"unhealthy".to_string()
|
|
||||||
},
|
|
||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
||||||
uptime_seconds: uptime,
|
|
||||||
connected: is_connected,
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&status).unwrap_or_else(|_| {
|
|
||||||
r#"{"status":"error","message":"Failed to serialize health status"}"#
|
|
||||||
.to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return 200 if connected, 503 if not
|
|
||||||
let http_status = if is_connected {
|
|
||||||
"200 OK"
|
|
||||||
} else {
|
|
||||||
"503 Service Unavailable"
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = format!(
|
|
||||||
"HTTP/1.1 {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
|
|
||||||
http_status,
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
crate::log_warn!("Failed to accept health check connection: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_health_status_serialization_healthy() {
|
|
||||||
let status = HealthStatus {
|
|
||||||
status: "healthy".to_string(),
|
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
uptime_seconds: 42,
|
|
||||||
connected: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&status).unwrap();
|
|
||||||
assert!(json.contains(r#""status":"healthy""#));
|
|
||||||
assert!(json.contains(r#""version":"0.1.0""#));
|
|
||||||
assert!(json.contains(r#""uptime_seconds":42"#));
|
|
||||||
assert!(json.contains(r#""connected":true"#));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_health_status_serialization_unhealthy() {
|
|
||||||
let status = HealthStatus {
|
|
||||||
status: "unhealthy".to_string(),
|
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
uptime_seconds: 100,
|
|
||||||
connected: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&status).unwrap();
|
|
||||||
assert!(json.contains(r#""status":"unhealthy""#));
|
|
||||||
assert!(json.contains(r#""connected":false"#));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_health_status_deserialization() {
|
|
||||||
let json = r#"{"status":"healthy","version":"0.1.0","uptime_seconds":42,"connected":true}"#;
|
|
||||||
let status: HealthStatus = serde_json::from_str(json).unwrap();
|
|
||||||
assert_eq!(status.status, "healthy");
|
|
||||||
assert_eq!(status.version, "0.1.0");
|
|
||||||
assert_eq!(status.uptime_seconds, 42);
|
|
||||||
assert!(status.connected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_health_status_clone() {
|
|
||||||
let status = HealthStatus {
|
|
||||||
status: "healthy".to_string(),
|
|
||||||
version: "0.1.0".to_string(),
|
|
||||||
uptime_seconds: 42,
|
|
||||||
connected: true,
|
|
||||||
};
|
|
||||||
let cloned = status.clone();
|
|
||||||
assert_eq!(status.status, cloned.status);
|
|
||||||
assert_eq!(status.version, cloned.version);
|
|
||||||
assert_eq!(status.uptime_seconds, cloned.uptime_seconds);
|
|
||||||
assert_eq!(status.connected, cloned.connected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: start_health_server is tested manually/via integration tests
|
|
||||||
// Unit testing it requires complex async server mocking which is not practical
|
|
||||||
}
|
|
||||||
11
src/main.rs
11
src/main.rs
|
|
@ -1,4 +1,3 @@
|
||||||
mod health;
|
|
||||||
mod ping;
|
mod ping;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod snmp;
|
mod snmp;
|
||||||
|
|
@ -197,10 +196,9 @@ async fn main() {
|
||||||
|
|
||||||
log_info!("WebSocket URL: {}", ws_url);
|
log_info!("WebSocket URL: {}", ws_url);
|
||||||
|
|
||||||
// Shared connection state for health check
|
// Shared connection state
|
||||||
// Starts as false (not connected), updated when WebSocket connects/disconnects
|
// Starts as false (not connected), updated when WebSocket connects/disconnects
|
||||||
let connected = Arc::new(AtomicBool::new(false));
|
let connected = Arc::new(AtomicBool::new(false));
|
||||||
let connected_for_health = Arc::clone(&connected);
|
|
||||||
|
|
||||||
// Create shutdown signal channel
|
// Create shutdown signal channel
|
||||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
|
|
@ -212,13 +210,6 @@ async fn main() {
|
||||||
let _ = shutdown_tx.send(true);
|
let _ = shutdown_tx.send(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start simple health endpoint with connection state
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = health::start_health_server(8080, connected_for_health).await {
|
|
||||||
log_warn!("Health server error: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retry loop with exponential backoff
|
// Retry loop with exponential backoff
|
||||||
let mut retry_delay = Duration::from_secs(1);
|
let mut retry_delay = Duration::from_secs(1);
|
||||||
let max_retry_delay = Duration::from_secs(60);
|
let max_retry_delay = Duration::from_secs(60);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue