feat(openclaw): modularize scripts, implement crons-as-code and EaC governance
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sync_crons.py
|
||||
Idempotently reconciles declarative OpenClaw automations defined in crons.yaml
|
||||
with the running OpenClaw gateway state.
|
||||
|
||||
Can run directly inside the container or remotely via kubectl exec.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
def find_crons_file():
|
||||
candidates = [
|
||||
os.environ.get("CRONS_FILE"),
|
||||
"crons.yaml",
|
||||
"k8s/openclaw/crons.yaml",
|
||||
"/home/node/.openclaw/scripts/crons.yaml",
|
||||
os.path.join(os.path.dirname(__file__), "../crons.yaml"),
|
||||
os.path.join(os.path.dirname(__file__), "crons.yaml"),
|
||||
]
|
||||
for c in candidates:
|
||||
if c and os.path.exists(c):
|
||||
return os.path.abspath(c)
|
||||
raise FileNotFoundError("Could not find crons.yaml. Pass via CRONS_FILE environment variable.")
|
||||
|
||||
def is_inside_container():
|
||||
return os.path.exists("/home/node/.openclaw") or os.environ.get("OPENCLAW_HOME") is not None
|
||||
|
||||
def run_openclaw_cmd(args):
|
||||
if is_inside_container():
|
||||
cmd = ["openclaw"] + args
|
||||
else:
|
||||
namespace = os.environ.get("NAMESPACE", "openclaw")
|
||||
pod = os.environ.get("POD_NAME", "openclaw-0")
|
||||
container = os.environ.get("CONTAINER_NAME", "openclaw")
|
||||
cmd = ["kubectl", "exec", "-n", namespace, pod, "-c", container, "--", "openclaw"] + args
|
||||
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(f"OpenClaw CLI command failed ({' '.join(cmd)}):\n{res.stderr}\n{res.stdout}")
|
||||
return res.stdout
|
||||
|
||||
def list_existing_jobs():
|
||||
raw = run_openclaw_cmd(["cron", "list", "--json"])
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
return data.get("jobs", [])
|
||||
except Exception as e:
|
||||
print(f"Warning: could not parse cron list json: {e}\nRaw output: {raw[:300]}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
def reconcile_jobs():
|
||||
file_path = find_crons_file()
|
||||
print(f"=== Syncing OpenClaw Crons-as-Code from {file_path} ===")
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
spec = yaml.safe_load(f)
|
||||
|
||||
declarations = spec.get("jobs", [])
|
||||
existing_jobs = list_existing_jobs()
|
||||
|
||||
print(f"Found {len(declarations)} declared jobs in YAML, {len(existing_jobs)} active jobs on gateway.")
|
||||
|
||||
for job in declarations:
|
||||
name = job.get("name")
|
||||
decl_key = job.get("declarationKey") or name
|
||||
desc = job.get("description", "")
|
||||
sched = job.get("schedule", {})
|
||||
|
||||
# Match existing job by declarationKey or exact name
|
||||
match = next((j for j in existing_jobs if j.get("declarationKey") == decl_key or j.get("name") == name), None)
|
||||
|
||||
if match:
|
||||
existing_id = match.get("id")
|
||||
print(f"Removing existing job '{name}' (ID {existing_id}) for idempotent recreation...")
|
||||
try:
|
||||
run_openclaw_cmd(["cron", "rm", existing_id])
|
||||
except Exception as e:
|
||||
print(f"Warning removing {existing_id}: {e}")
|
||||
|
||||
# Build add command
|
||||
cmd = [
|
||||
"cron", "add",
|
||||
"--name", name,
|
||||
"--declaration-key", decl_key,
|
||||
"--description", desc,
|
||||
]
|
||||
|
||||
if sched.get("kind") == "every":
|
||||
cmd.extend(["--every", sched.get("every", "15m")])
|
||||
elif sched.get("kind") == "cron":
|
||||
cmd.extend(["--cron", sched.get("expr", "* * * * *")])
|
||||
if sched.get("tz"):
|
||||
cmd.extend(["--tz", sched.get("tz")])
|
||||
|
||||
if job.get("session"):
|
||||
cmd.extend(["--session", job.get("session")])
|
||||
|
||||
if job.get("delivery") == "none":
|
||||
cmd.append("--no-deliver")
|
||||
|
||||
if job.get("command"):
|
||||
cmd.extend(["--command", job.get("command")])
|
||||
|
||||
if job.get("agent"):
|
||||
cmd.extend(["--agent", job.get("agent")])
|
||||
|
||||
if job.get("model"):
|
||||
cmd.extend(["--model", job.get("model")])
|
||||
|
||||
if job.get("channel"):
|
||||
cmd.extend(["--channel", job.get("channel")])
|
||||
|
||||
if job.get("to"):
|
||||
cmd.extend(["--to", job.get("to")])
|
||||
|
||||
if job.get("threadId"):
|
||||
cmd.extend(["--thread-id", str(job.get("threadId"))])
|
||||
|
||||
if job.get("announce"):
|
||||
cmd.append("--announce")
|
||||
|
||||
if job.get("message"):
|
||||
cmd.extend(["--message", job.get("message")])
|
||||
|
||||
print(f"Registering job '{name}'...")
|
||||
run_openclaw_cmd(cmd)
|
||||
print(f"Successfully registered '{name}'.")
|
||||
|
||||
print("=== OpenClaw Crons-as-Code reconciliation complete! ===")
|
||||
|
||||
if __name__ == "__main__":
|
||||
reconcile_jobs()
|
||||
Reference in New Issue
Block a user