Version 1.0
This commit is contained in:
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# 1. Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 2. Install Python deps
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN pip install --no-cache-dir pyyaml
|
||||||
|
|
||||||
|
# 3. Install Supercronic
|
||||||
|
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.29/supercronic-linux-amd64 \
|
||||||
|
SUPERCRONIC=supercronic-linux-amd64 \
|
||||||
|
SUPERCRONIC_SHA1SUM=cd48d45c4b10f3f0bfdd3a57d054cd05ac96812b
|
||||||
|
|
||||||
|
RUN curl -fsSLO "$SUPERCRONIC_URL" \
|
||||||
|
&& echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \
|
||||||
|
&& chmod +x "$SUPERCRONIC" \
|
||||||
|
&& mv "$SUPERCRONIC" /usr/local/bin/supercronic
|
||||||
|
|
||||||
|
# 4. Setup App Logic
|
||||||
|
WORKDIR /app
|
||||||
|
COPY manager.py /app/manager.py
|
||||||
|
COPY log_formatter.py /app/log_formatter.py
|
||||||
|
|
||||||
|
# 5. Setup Defaults
|
||||||
|
# We create a separate folder to hold the "template" files
|
||||||
|
RUN mkdir /app/defaults
|
||||||
|
COPY defaults/config.yaml /app/defaults/config.yaml
|
||||||
|
COPY defaults/hello_world.py /app/defaults/hello_world.py
|
||||||
|
|
||||||
|
# 6. Prepare the mount point
|
||||||
|
RUN mkdir /scripts
|
||||||
|
VOLUME ["/scripts"]
|
||||||
|
|
||||||
|
ENTRYPOINT ["python3", "-u", "/app/manager.py"]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Current Features:
|
||||||
|
- run python-scripts in the /scripts
|
||||||
|
- install dependencies
|
||||||
|
- config via config-file in /scripts
|
||||||
|
- auto-populate /scripts if empty
|
||||||
|
- hello_world.py demo in scripts folder
|
||||||
|
- scripts print to container logs
|
||||||
|
|
||||||
|
Planned Features:
|
||||||
|
|||||||
8
defaults/config.yaml
Normal file
8
defaults/config.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 1. List libraries to install at startup
|
||||||
|
dependencies:
|
||||||
|
- datetime
|
||||||
|
|
||||||
|
# 2. Define jobs (Script Name + Cron Schedule)
|
||||||
|
jobs:
|
||||||
|
- script: "hello_world.py"
|
||||||
|
schedule: "* * * * *" # Runs every minute
|
||||||
3
defaults/hello_world.py
Normal file
3
defaults/hello_world.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import datetime
|
||||||
|
print("Hello world")
|
||||||
|
print(f"the current date & time is: {datetime.datetime.now()}")
|
||||||
39
log_formatter.py
Normal file
39
log_formatter.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Regex zum Finden des Script-Namens im Command
|
||||||
|
# Sucht nach: job.command="... /scripts/(DATEINAME)"
|
||||||
|
script_pattern = re.compile(r'job\.command=".*?/scripts/([^"]+)"')
|
||||||
|
|
||||||
|
# Regex zum Finden der eigentlichen Nachricht
|
||||||
|
# Sucht nach: msg="NACHRICHT"
|
||||||
|
msg_pattern = re.compile(r'msg="(.*?)"')
|
||||||
|
|
||||||
|
def clean_log():
|
||||||
|
# Liest Standard-Input (Pipe von Supercronic)
|
||||||
|
for line in sys.stdin:
|
||||||
|
# Wir interessieren uns nur für Zeilen, die von unseren Scripts kommen
|
||||||
|
# (channel=stdout oder channel=stderr)
|
||||||
|
if 'channel=stdout' in line or 'channel=stderr' in line:
|
||||||
|
|
||||||
|
# Script Namen extrahieren
|
||||||
|
script_match = script_pattern.search(line)
|
||||||
|
script_name = script_match.group(1) if script_match else "Unknown"
|
||||||
|
|
||||||
|
# Nachricht extrahieren
|
||||||
|
msg_match = msg_pattern.search(line)
|
||||||
|
message = msg_match.group(1) if msg_match else line.strip()
|
||||||
|
|
||||||
|
# Gewünschtes Format ausgeben: [script.py] Nachricht
|
||||||
|
print(f"[{script_name}] {message}")
|
||||||
|
|
||||||
|
# WICHTIG: Sofort flushen, damit Logs nicht verzögert erscheinen
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
elif 'level=error' in line or 'level=warning' in line:
|
||||||
|
# System-Fehler von Supercronic (z.B. Syntaxfehler im Cron) trotzdem anzeigen
|
||||||
|
print(f"[SYSTEM] {line.strip()}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
clean_log()
|
||||||
84
manager.py
Normal file
84
manager.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import yaml
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
SCRIPTS_DIR = "/scripts"
|
||||||
|
DEFAULTS_DIR = "/app/defaults"
|
||||||
|
CONFIG_PATH = os.path.join(SCRIPTS_DIR, "config.yaml")
|
||||||
|
CRONTAB_PATH = "/app/generated_crontab"
|
||||||
|
|
||||||
|
def initialize_volume_if_empty():
|
||||||
|
"""Copies default files to /scripts if config.yaml is missing."""
|
||||||
|
if not os.path.exists(CONFIG_PATH):
|
||||||
|
print("---------------------------------")
|
||||||
|
print("No config found in volume. Initializing with defaults...")
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
os.makedirs(SCRIPTS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Copy all files from defaults to scripts
|
||||||
|
if os.path.exists(DEFAULTS_DIR):
|
||||||
|
for filename in os.listdir(DEFAULTS_DIR):
|
||||||
|
src = os.path.join(DEFAULTS_DIR, filename)
|
||||||
|
dst = os.path.join(SCRIPTS_DIR, filename)
|
||||||
|
if os.path.isfile(src):
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
print(f" -> Created: {filename}")
|
||||||
|
print("Initialization complete.")
|
||||||
|
print("---------------------------------")
|
||||||
|
else:
|
||||||
|
print(f"Found existing config at {CONFIG_PATH}. Using it.")
|
||||||
|
|
||||||
|
def install_dependencies(deps):
|
||||||
|
if not deps:
|
||||||
|
return
|
||||||
|
print(f"Installing {len(deps)} dependencies...")
|
||||||
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-cache-dir"] + deps)
|
||||||
|
|
||||||
|
def generate_crontab(jobs):
|
||||||
|
print(f"Scheduling {len(jobs)} jobs...")
|
||||||
|
lines = []
|
||||||
|
for job in jobs:
|
||||||
|
schedule = job['schedule']
|
||||||
|
script = job['script']
|
||||||
|
# Use -u for unbuffered output
|
||||||
|
command = f"python3 -u {SCRIPTS_DIR}/{script}"
|
||||||
|
lines.append(f"{schedule} {command}")
|
||||||
|
print(f" -> Loaded: {script} [{schedule}]")
|
||||||
|
|
||||||
|
with open(CRONTAB_PATH, "w") as f:
|
||||||
|
f.write("\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1. Initialize Volume (The new feature)
|
||||||
|
initialize_volume_if_empty()
|
||||||
|
|
||||||
|
# 2. Read Config
|
||||||
|
if not os.path.exists(CONFIG_PATH):
|
||||||
|
print(f"Error: {CONFIG_PATH} still not found after initialization.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(CONFIG_PATH, "r") as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# 3. Setup Environment
|
||||||
|
install_dependencies(config.get("dependencies", []))
|
||||||
|
generate_crontab(config.get("jobs", []))
|
||||||
|
|
||||||
|
# 4. Start Supercronic
|
||||||
|
print("Starting Scheduler...")
|
||||||
|
print("--------------------------------------------")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# Wir starten Supercronic und leiten stderr (wo Logs sind) an stdout weiter
|
||||||
|
# und pipen das Ganze in unseren log_formatter.py
|
||||||
|
cmd = f"supercronic {CRONTAB_PATH} 2>&1 | python3 -u /app/log_formatter.py"
|
||||||
|
|
||||||
|
# shell=True ist hier notwendig für die Pipe "|"
|
||||||
|
subprocess.call(cmd, shell=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user