# 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 [], }, }