diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 7b01bbe..c250169 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to GitLab Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io @@ -39,4 +39,4 @@ jobs: platforms: linux/amd64,linux/arm64 - name: Do not automatically disable workflow execution - uses: gautamkrishnar/keepalive-workflow@v1 + uses: gautamkrishnar/keepalive-workflow@v1 \ No newline at end of file diff --git a/build/Cargo.toml b/build/Cargo.toml new file mode 100644 index 0000000..fff21a0 --- /dev/null +++ b/build/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cronjoblistener" +version = "0.1.0" +edition = "2021" + +[profile.release] +opt-level = "z" +lto = true +strip = true + +[dependencies] +bollard = "0.17" +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1.0" +dotenv = "0.15" +log = "0.4" +env_logger = "0.11" +futures-util = "0.3" \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile index 882cfab..aa3166a 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,6 +1,21 @@ -FROM python:3-slim +FROM rust:1-slim AS builder WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY main.py . -CMD ["python", "./main.py"] + +# Kopiere die Cargo.toml und den Quellcode ins Arbeitsverzeichnis +COPY Cargo.toml . +COPY src ./src + +# Installiere notwendige Tools +RUN apt-get update && apt-get install -y musl-tools + +# Erstelle den Build für beide Architekturen (amd64 und aarch64) +ARG TARGETARCH + +RUN rustup target add ${TARGETARCH}-unknown-linux-musl && \ + cargo build --release --target ${TARGETARCH}-unknown-linux-musl + +# Der finale Stage, der das Ergebnis des Builds verwendet +FROM scratch +COPY --from=builder /app/target/${TARGETARCH}-unknown-linux-musl/release/cronjoblistener /app/cronjoblistener + +ENTRYPOINT ["/app/cronjoblistener"] \ No newline at end of file diff --git a/build/main.py b/build/main.py deleted file mode 100644 index 9a792e7..0000000 --- a/build/main.py +++ /dev/null @@ -1,43 +0,0 @@ -import docker, os -from docker.errors import APIError, NotFound - -label_key = os.getenv("LABEL_KEY", "ofelia.restart") -label_value = os.getenv("LABEL_VALUE", "true") -container_name_to_restart = os.getenv("CRON_CONTAINER", "cronjobs-cron-1") - -client = docker.from_env() - -def restart_container(container_name): - try: - container = client.containers.get(container_name) - container.restart() - print(f"Container {container_name} was restarted.") - except NotFound: - print(f"Container {container_name} not found.") - except APIError as e: - print(f"API error when restarting container {container_name}: {e}") - -def handle_event(event): - if event.get("Type") == "container" and event.get("Action") in ["start", "stop"]: - container_id = event.get("Actor").get("ID") - try: - container = client.containers.get(container_id) - labels = container.labels - if labels.get(label_key) == label_value: - print(f"{container.name} {event.get('Action')}ed") - restart_container(container_name_to_restart) - except NotFound: - print(f"Container {container_id} not found.") - except APIError as e: - print(f"API error when getting container {container_id}: {e}") - -def main(): - print("Listening on container start and stop events...") - try: - for event in client.events(decode=True): - handle_event(event) - except Exception as e: - print(f"Unknown error: {e}") - -if __name__ == "__main__": - main() diff --git a/build/requirements.txt b/build/requirements.txt deleted file mode 100644 index ae3daa6..0000000 --- a/build/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -docker~=7.0 diff --git a/build/src/main.rs b/build/src/main.rs new file mode 100644 index 0000000..e7370bd --- /dev/null +++ b/build/src/main.rs @@ -0,0 +1,139 @@ +use bollard::Docker; +use bollard::models::EventMessage; +use bollard::errors::Error as DockerError; +use dotenv::dotenv; +use futures_util::StreamExt; +use std::env; +use std::sync::Arc; +use tokio::sync::Mutex; +use log::{info, debug, error, warn}; +use tokio::time::{sleep, Duration}; + +#[tokio::main] +async fn main() { + // Lese Umgebungsvariablen (z.B. für LABEL_KEY und LABEL_VALUE) + dotenv().ok(); + + // Setze das Standard-Log-Level auf "info", wenn keine Umgebungsvariable RUST_LOG gesetzt ist + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", "info"); + } + + // Initialisiere den Logger ohne Zeitstempel + env_logger::Builder::from_default_env() + .format_timestamp(None) + .init(); + + info!("Listening on container start and stop events..."); + + let label_key = env::var("LABEL_KEY").unwrap_or_else(|_| "ofelia.restart".to_string()); + let label_value = env::var("LABEL_VALUE").unwrap_or_else(|_| "true".to_string()); + let container_name_to_restart = env::var("CRON_CONTAINER").unwrap_or_else(|_| "cronjobs-cron-1".to_string()); + + let docker = Docker::connect_with_local_defaults().expect("Failed to connect to Docker"); + + // Mutex für die Steuerung des Neustart-Timers + let restart_timer = Arc::new(Mutex::new(None)); + + let mut events_stream = docker.events::(None).fuse(); + + while let Some(event) = events_stream.next().await { + match event { + Ok(event_message) => { + debug!("Received event: {:?}", event_message); + let docker = docker.clone(); + let restart_timer = restart_timer.clone(); + let label_key = label_key.clone(); + let label_value = label_value.clone(); + let container_name_to_restart = container_name_to_restart.clone(); + + tokio::spawn(async move { + handle_event(&docker, event_message, &label_key, &label_value, &container_name_to_restart, restart_timer).await; + }); + }, + Err(e) => error!("Error receiving event: {:?}", e), + } + } +} + +async fn handle_event( + docker: &Docker, + event: EventMessage, + label_key: &str, + label_value: &str, + container_name_to_restart: &str, + restart_timer: Arc>>> +) { + if let Some(action) = event.action { + if action == "start" || action == "stop" { + if let Some(actor) = event.actor { + if let Some(container_id) = actor.id { + // Hole den Container-Namen + let container_info = docker.inspect_container(&container_id, None).await; + if let Ok(container_info) = container_info { + if let Some(container_name) = container_info.name { + // Ignoriere Events des Cron-Containers selbst + if container_name == format!("/{}", container_name_to_restart) { + debug!("Ignoring event for the cron container itself: {}", container_name); + return; + } + } + } + + match docker.inspect_container(&container_id, None).await { + Ok(container_info) => { + if let Some(labels) = container_info.config.and_then(|c| c.labels) { + if labels.get(label_key).map_or(false, |v| v == label_value) { + info!("Container {} {}ed", container_info.name.unwrap_or_default(), action); + + // Setze den Timer zurück, wenn ein neuer Container startet oder stoppt + let mut timer_guard = restart_timer.lock().await; + if let Some(existing_timer) = timer_guard.take() { + existing_timer.abort(); // Abbrechen des bestehenden Timers + info!("Timer reset for restarting the cron container"); + } + + let docker = docker.clone(); + let container_name_to_restart = container_name_to_restart.to_string(); + + // Starte einen neuen Timer (z.B. 60 Sekunden) + *timer_guard = Some(tokio::spawn(async move { + info!("Timer set for restarting the cron container in 60 seconds"); + sleep(Duration::from_secs(60)).await; + if let Err(e) = restart_container(&docker, &container_name_to_restart).await { + error!("Error restarting container {}: {:?}", container_name_to_restart, e); + } + })); + } + } + }, + Err(DockerError::DockerResponseServerError { status_code, .. }) if status_code == 404 => { + warn!("Container {} not found", container_id); + }, + Err(e) => { + error!("API error when getting container {}: {:?}", container_id, e); + } + } + } + } + } + } +} + +async fn restart_container(docker: &Docker, container_name: &str) -> Result<(), DockerError> { + match docker.inspect_container(container_name, None).await { + Ok(_) => { + docker.restart_container(container_name, None).await?; + info!("Container {} was restarted.", container_name); + Ok(()) + }, + Err(DockerError::DockerResponseServerError { status_code, .. }) if status_code == 404 => { + warn!("Container {} not found.", container_name); + Ok(()) + }, + Err(e) => { + error!("API error when restarting container {}: {:?}", container_name, e); + Err(e) + } + } +} \ No newline at end of file