provisioning/schemas/lib/scheduler/scheduler.ncl

148 lines
5.1 KiB
Text

# Generic scheduler helper — produces a scheduling artefact for any of the
# four runtime targets (K8s CronJob, systemd timer, cron.d entry, daemon
# task registration). Not coupled to backup nor to Kubernetes; any task in
# the repo that needs to be scheduled can build on top of this.
#
# Example:
# let s = (import "scheduler.ncl").make_schedule {
# name = "etcd-snapshot",
# schedule_kind = 'cron, cron_expr = "0 */6 * * *",
# target = { kind = 'systemd_timer, host_selector = "control_planes",
# user = "root", unit_name = "prvng-etcd-snapshot" },
# command = "/usr/local/bin/prvng-backup one-shot backup etcd-snapshot",
# env = { …secret refs… },
# } in s.systemd_units
{
# === Target descriptors ===================================================
K8sCronJobTarget = {
kind | [| 'k8s_cronjob |],
namespace | String,
image | String,
image_pull_policy | [| 'IfNotPresent, 'Always, 'Never |] | default = 'IfNotPresent,
service_account | String | optional,
node_selector | { _ | String } | default = {},
restart_policy | [| 'OnFailure, 'Never |] | default = 'OnFailure,
successful_jobs_history_limit | Number | default = 3,
failed_jobs_history_limit | Number | default = 5,
},
SystemdTimerTarget = {
kind | [| 'systemd_timer |],
unit_name | String,
host_selector | String | doc "Hostname pattern or role (e.g. 'control_planes')",
user | String | default = "root",
after | Array String | default = ["network-online.target"],
persistent | Bool | default = true,
},
CronDTarget = {
kind | [| 'cron_d |],
file_name | String | doc "Filename under /etc/cron.d/",
host_selector | String,
user | String | default = "root",
},
DaemonTaskTarget = {
kind | [| 'daemon_task |],
task_id | String,
daemon_endpoint | String | default = "unix:///run/prvng-backup.sock",
},
# === Top-level builder ====================================================
# make_schedule returns a record with one populated branch out of:
# { manifests, systemd_units, cron_files, daemon_registrations }.
# Callers serialise the appropriate branch.
make_schedule = fun spec =>
let target_kind = spec.target.kind in
let cron_expr = spec.cron_expr in
let name = spec.name in
let command = spec.command in
let env = spec.env in
{
manifests =
if target_kind == 'k8s_cronjob then
[{
apiVersion = "batch/v1",
kind = "CronJob",
metadata = {
name = name,
namespace = spec.target.namespace,
},
spec = {
schedule = cron_expr,
successfulJobsHistoryLimit = spec.target.successful_jobs_history_limit,
failedJobsHistoryLimit = spec.target.failed_jobs_history_limit,
jobTemplate.spec.template.spec = {
restartPolicy = std.string.from_enum spec.target.restart_policy,
serviceAccountName = spec.target.service_account,
nodeSelector = spec.target.node_selector,
containers = [{
name = name,
image = spec.target.image,
imagePullPolicy = std.string.from_enum spec.target.image_pull_policy,
command = ["/bin/sh", "-c", command],
env = std.record.to_array env
|> std.array.map (fun e => { name = e.field, value = e.value }),
}],
},
},
}]
else [],
systemd_units =
if target_kind == 'systemd_timer then
[{
host_selector = spec.target.host_selector,
unit_name = spec.target.unit_name,
service_unit = m%"
[Unit]
Description=%{name}
After=%{std.string.join " " spec.target.after}
[Service]
Type=oneshot
User=%{spec.target.user}
ExecStart=%{command}
EnvironmentFile=-/etc/prvng-backup/%{name}.env
"%,
timer_unit = m%"
[Unit]
Description=Timer for %{name}
[Timer]
OnCalendar=%{cron_expr}
Persistent=%{if spec.target.persistent then "true" else "false"}
[Install]
WantedBy=timers.target
"%,
}]
else [],
cron_files =
if target_kind == 'cron_d then
[{
host_selector = spec.target.host_selector,
path = "/etc/cron.d/%{spec.target.file_name}",
content = m%"
%{cron_expr} %{spec.target.user} %{command}
"%,
}]
else [],
daemon_registrations =
if target_kind == 'daemon_task then
[{
task_id = spec.target.task_id,
daemon_endpoint = spec.target.daemon_endpoint,
schedule = cron_expr,
command = command,
env = env,
}]
else [],
},
}