cron

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 15 Imported by: 0

README

Cron generator integration

This directory contains the optional cron generator integration for Melody.

A declarative cron.Configuration registry maps CLI command names to schedules. A single meta command (melody:cron:generate) consumes that registry and writes a deterministic crontab file. Commands themselves stay plain Melody CLI commands — they do not implement any cron-specific interface, so a command's implementation and its scheduling concerns live in different files.

The integration is published as independent Go modules so applications pull only the binding matching their melody version:

  • Melody v1 binding: this directory — github.com/precision-soft/melody/integrations/cron
  • Melody v2 binding: ./v2/github.com/precision-soft/melody/integrations/cron/v2
  • Melody v3 binding: ./v3/github.com/precision-soft/melody/integrations/cron/v3

The three bindings share the same exported API and behavior; they differ only in the melody version they import. The examples below use the v3 import path; for v2, replace /v3 with /v2; for v1, drop the /v3 suffix entirely (the v1 binding lives at the module root, following Go's no-suffix convention for v0/v1).

What you get

  • A declarative configuration registry (cron.Configuration) — userland code maps a command name to an EntryConfig (schedule + per-command overrides) via a fluent builder: cron.NewConfiguration().Schedule(commandName, &cron.EntryConfig{...}).Schedule(...).
  • A type-safe command-name helper (cron.CommandName[T](constructor)) — instantiates a constructor and returns its Name() so the registry references commands by constructor instead of hardcoded strings.
  • A melody:cron:generate command (cron.GenerateCommand) constructed via cron.NewGenerateCommand(configuration).
  • A cron.Render(entries, options) (string, error) renderer (free-function wrapper in ./template.go that dispatches to the built-in crontab template in ./template_crontab.go) that validates entries and produces the crontab text. New output formats are added by dropping a new template_<format>.go next to template_crontab.go, implementing the cron.Template interface, and registering the constructor in BuiltinTemplates().
  • Container-parameter cascade (./parameter.go) — every flag falls back to a configurable parameter.
  • Per-command overrides on EntryConfig: each entry can pick its own destination file, custom command parts, log file name, instance count, or disable logging entirely.
  • Custom heartbeat command: RenderOptions.HeartbeatCommand replaces the default /bin/touch <path> heartbeat line with any command shape (e.g. an HTTP ping).

Binary resolution

The crontab entries reference the built application binary itself (resolved via os.Executable() at run time, or overridden with --binary / melody.cron.binary). Run melody:cron:generate from the production binary — invoking it through go run resolves the binary to a temporary build path that disappears once the process exits, which is almost never what you want in crontab.

Install

Pick the binding that matches your melody version:

# Melody v1
go get github.com/precision-soft/melody/integrations/cron@latest

# Melody v2
go get github.com/precision-soft/melody/integrations/cron/v2@latest

# Melody v3
go get github.com/precision-soft/melody/integrations/cron/v3@latest

Each binding depends on its respective melody module — github.com/precision-soft/melody for v1, github.com/precision-soft/melody/v2 for v2, github.com/precision-soft/melody/v3 for v3 — plus github.com/urfave/cli/v3 (the urfave/cli major version is shared across all three bindings).

Configuration parameters

The cascade for every flag is CLI flag (explicitly set) → container parameter. There are no hardcoded fallbacks inside the cron command itself; defaults live entirely in the parameter system.

The parameter names are exposed as constants:

Constant Parameter name CLI flag Default from RegisterDefaultParameters
cron.ParameterDestinationFile melody.cron.destination_file --out %kernel.project_dir%/generated_conf/cron/crontab
cron.ParameterLogsDir melody.cron.logs_dir --logs-dir %kernel.logs_dir%/cron
cron.ParameterUser melody.cron.user --user not registered — userland must supply a user via --user, melody.cron.user, or per-command EntryConfig.User, otherwise generation fails with ErrEntryEmptyUser (or ErrHeartbeatUserMissing when a heartbeat is configured)
cron.ParameterBinary melody.cron.binary --binary not registered — falls through to os.Executable() when both flag and parameter are empty
cron.ParameterHeartbeatPath melody.cron.heartbeat_path --heartbeat-path not registered — heartbeat disabled by default
cron.ParameterHeartbeatAutoEnabled melody.cron.heartbeat.enabled no flag not registered — when truthy (parsed by Parameter.Bool(): true/1/yes/on, case-insensitive, trimmed) and neither --heartbeat-path nor melody.cron.heartbeat_path is set, the heartbeat path defaults to <logs-dir>/heartbeat.crontab. See Auto-derive the heartbeat path
cron.ParameterTemplate melody.cron.template --template crontab (set by RegisterDefaultParameters)
no parameter no parameter --heartbeat-command repeatable; each value is one argv token of a custom heartbeat command. When set, overrides --heartbeat-path
no parameter no parameter --heartbeat-destination repeatable; restricts the heartbeat to the listed destinations. Values: default (the --out file), an absolute path, or a relative path matched against dir(--out). When unset, the heartbeat goes to every destination

Parameters are looked up only when the matching CLI flag was not explicitly set (urfave's IsSet).

--heartbeat-command and --heartbeat-destination are CLI-only — they have no container-parameter fallback. The other flags (--out, --logs-dir, --user, --binary, --heartbeat-path, --template) cascade through the parameter system as described above.

Cron expression validation

The generator does not parse Schedule.Minute / Hour / DayOfMonth / Month / DayOfWeek against the crontab grammar. The only field-level checks performed at generation time are:

  • embedded whitespace (space, tab, newline, carriage return) is rejected — crontab fields must be single tokens;
  • embedded %, \n, \r are rejected anywhere in a rendered token (they would split or escape the line).

Anything else passes through verbatim. That includes inputs that the cron daemon will silently reject at install time, e.g. Minute: "99", Hour: "abc", DayOfMonth: "*/0", or unbalanced ranges like "5-3". Validate the output before deploying — either with crontab -T /path/to/generated/crontab on the target host (-T is GNU cronie's syntax-only check), with crontab.guru, or with a unit test that asserts on the generated entries.

Keeping the validation surface minimal is a deliberate trade-off: a full grammar parser would either duplicate the cron daemon's own implementation (and inevitably diverge from real cronie/vixie-cron/bsd-cron quirks) or pull in a third-party dependency for a one-shot check.

melody:cron:generate errors out when:

  • melody.cron.destination_file (and --out) are both empty — cron: no output path configured
  • a command has LogDisabled=false but melody.cron.logs_dir (and --logs-dir) are both empty — cron: command "X" wants log redirection but no logs-dir is configured
  • a rendered entry ends up with an empty Usercron: command "X" has no user; set EntryConfig.User on the schedule, pass --user, or register the melody.cron.user parameter
Pre-built defaults via RegisterDefaultParameters

The integration ships an opt-in helper that registers safe path/template defaults from the table above (%kernel.project_dir% and %kernel.logs_dir% are resolved by melody automatically — see Project directory resolution below). melody.cron.user is not registered by the helper because there is no sensible default — the integration has no idea which OS user should run your crontab. Userland must register it (or supply EntryConfig.User / --user).

import (
    melodyapplicationcontract "github.com/precision-soft/melody/v3/application/contract"
    melodycron "github.com/precision-soft/melody/integrations/cron/v3"
)

func (instance *Module) RegisterParameters(registrar melodyapplicationcontract.ParameterRegistrar) {
    melodycron.RegisterDefaultParameters(registrar)

    registrar.RegisterParameter(melodycron.ParameterUser, "%env(COMMANDS_USER)%")
}
Custom defaults via env-driven parameters

If you'd rather wire each parameter to your own .env variables, skip the helper and register them directly:

# .env
COMMANDS_USER=www-data
COMMANDS_LOGS_DIR=%kernel.logs_dir%/cron
func (instance *Module) RegisterParameters(registrar melodyapplicationcontract.ParameterRegistrar) {
    registrar.RegisterParameter(melodycron.ParameterUser, "%env(COMMANDS_USER)%")
    registrar.RegisterParameter(melodycron.ParameterLogsDir, "%env(COMMANDS_LOGS_DIR)%")
    // melody.cron.destination_file omitted — supply via --out at run time or register here
}

Parameter resolution is recursive: COMMANDS_LOGS_DIR=%kernel.logs_dir%/cron reads the built-in kernel.logs_dir parameter and substitutes it before the value reaches the cron command.

Project directory resolution (uncompiled vs compiled)

melody computes kernel.project_dir at boot:

  • Uncompiled (go run .) — finds the project root by climbing up from the working directory until a go.mod is found. The crontab and log paths use that root, so melody:cron:generate writes under your source tree.
  • Compiled binary — uses the directory containing the binary (resolved through os.Executable() + EvalSymlinks). The crontab and log paths use that directory, which is typically the deployment root.

That means %kernel.project_dir%/generated_conf/cron/crontab works unchanged in both modes; you don't need different defaults for dev and prod.

Production workflow

Build and run the binary from its deployment root:

go build -o /opt/myapp/myapp .
/opt/myapp/myapp melody:cron:generate
# writes /opt/myapp/generated_conf/cron/crontab with entries pointing at /opt/myapp/myapp
Development workflow (go run .)

go run builds an ephemeral binary under $GOCACHE/.../go-build*/.... The output crontab and log paths still resolve correctly via the project root (because the cron command falls through to "find the nearest go.mod"), but the binary path inside each entry points at the temporary build, which evaporates as soon as the process exits.

For a usable crontab in dev mode, pass --binary (or register melody.cron.binary) so the entries reference your deployed binary path instead:

go run . melody:cron:generate --binary=/opt/myapp/myapp
# writes <project>/generated_conf/cron/crontab with entries pointing at /opt/myapp/myapp

Both modes have been verified end-to-end with a minimal melody v3 application.

Per-command overrides

An EntryConfig value can opt in to additional per-command behavior beyond the cron expression itself:

Field Effect
Schedule pointer to the 5-field *cron.Schedule (Minute, Hour, DayOfMonth, Month, DayOfWeek). When nil, the entry runs every minute (* * * * *)
User run this entry as a specific system user instead of the default --user
LogFileName use a custom file name (within --logs-dir) instead of the sanitized command name
LogFileNameRaw when auto-deriving the log file name (no LogFileName set), keep : and other non-/ characters; only / is replaced for filesystem safety. Default behavior sanitizes :-
LogDisabled omit the >> '<log>' 2>&1 redirection entirely for this entry
DestinationFile route this entry to a different crontab file (relative paths join dir(--out); absolute paths are used verbatim). The generator writes one file per distinct destination and emits the heartbeat into each
Command replace <binary> <command-name> with a custom argv slice (e.g. wrap with /usr/bin/flock, nice, php, or substitute the whole command). When set, the --binary cascade is ignored for this entry
Instances when set to N > 1, the generator expands the schedule into N entries with --max-instances=N --instance-index=I flags appended to the default args (skipped when Command is set) and a per-instance -I suffix on the log file. Use it to parallelize the same command across multiple workers

Example:

cronConfiguration.Schedule("billing:run", &melodycron.EntryConfig{
    Schedule:        &melodycron.Schedule{Minute: "0", Hour: "2"},
    User:            "billing",
    DestinationFile: "billing-crontab",
    Command:         []string{"/usr/bin/flock", "-n", "/var/run/billing.lock", "/opt/app", "billing:run"},
    LogFileName:     "billing.log",
})

Auto-derive the heartbeat path

Set melody.cron.heartbeat.enabled (constant: cron.ParameterHeartbeatAutoEnabled) to a truthy value to opt in to auto-deriving the heartbeat path from --logs-dir whenever neither --heartbeat-path nor melody.cron.heartbeat_path is set. The value is parsed by the framework's Parameter.Bool() helper, which trims whitespace and accepts true/false, 1/0, yes/no, on/off (case-insensitive). Anything else — including the empty string or a malformed value — leaves the heartbeat disabled.

The parameter only controls the auto-derive fallback; it does not gate the heartbeat when an explicit path is set. An explicit --heartbeat-path flag or melody.cron.heartbeat_path parameter always emits a heartbeat, regardless of melody.cron.heartbeat.enabled.

# .env
APP_CRON_HEARTBEAT_AUTO_ENABLED=true
func (instance *Module) RegisterParameters(registrar melodyapplicationcontract.ParameterRegistrar) {
    registrar.RegisterParameter(melodycron.ParameterHeartbeatAutoEnabled, "%env(APP_CRON_HEARTBEAT_AUTO_ENABLED)%")
}

With the opt-in active and --logs-dir=/var/log/cron, the heartbeat path resolves to /var/log/cron/heartbeat.crontab — the heartbeat line then renders as * * * * * <user> /bin/touch /var/log/cron/heartbeat.crontab and is appended to every destination file by default (restrict it with --heartbeat-destination).

Precedence (highest → lowest):

  1. --heartbeat-path=<path> (explicit flag wins, even when melody.cron.heartbeat.enabled=true).
  2. melody.cron.heartbeat_path parameter (explicit container parameter).
  3. melody.cron.heartbeat.enabled opt-in + non-empty --logs-dir<logs-dir>/heartbeat.crontab.
  4. Otherwise the heartbeat stays disabled (no parameter, no flag, no fallback).

RegisterDefaultParameters does not register melody.cron.heartbeat.enabled — the helper only registers safe, environment-agnostic defaults, and "emit a heartbeat" is an environment decision. Userland opts in explicitly.

--heartbeat-command (the custom heartbeat) is independent: it overrides --heartbeat-path (resolved or auto-derived) just like the documented heartbeat-path cascade, so setting melody.cron.heartbeat.enabled=true and --heartbeat-command=/usr/bin/curl ... together still produces the custom command line, not the auto-derived touch.

Custom heartbeat command

--heartbeat-path produces the simple /bin/touch <path> line. For anything else (e.g. HTTP pings, custom commands), repeat --heartbeat-command once per argv token:

./app melody:cron:generate \
    --out=generated_conf/cron/crontab \
    --logs-dir=var/log/cron \
    --user=www-data \
    --heartbeat-command=/usr/bin/curl \
    --heartbeat-command=-fsS \
    --heartbeat-command=https://healthcheck.example.com/ping

Equivalent at the API level via cron.Render:

content, err := cron.Render(entries, cron.RenderOptions{
    HeartbeatUser:    "monitor",
    HeartbeatCommand: []string{"/usr/bin/curl", "-fsS", "https://healthcheck.example.com/ping"},
})

When both HeartbeatPath and HeartbeatCommand are set, HeartbeatCommand wins.

Heartbeat per-destination targeting

When schedules route entries to several destination files (via EntryConfig.DestinationFile), the heartbeat goes to every destination that is written. Restrict it to specific files with one or more --heartbeat-destination=<value> flags:

# heartbeat only in the default crontab
./app melody:cron:generate --heartbeat-path=/var/log/cron/heartbeat ... \
    --heartbeat-destination=default

# heartbeat in a specific custom destination
./app melody:cron:generate --heartbeat-path=/var/log/cron/heartbeat ... \
    --heartbeat-destination=billing-crontab

Accepted values: default (the --out destination), an absolute path, or a relative path matched against dir(--out). Each value must match a destination actually being written — misspelled values error out instead of being silently ignored.

Customizing the template

The generator dispatches rendering to a registered Template whose Name() matches --template (or, if unset, the melody.cron.template container parameter; default "crontab"). The crontab template ships in-tree and is registered automatically. You can plug in your own (Kubernetes CronJob, Supervisor, custom YAML/INI, etc.) without forking the cron integration.

Template interface
type Template interface {
    Name() string
    Render(entries []Entry, options RenderOptions) (string, error)
}

Render is called once per output destination — entries are already expanded for multi-instance and have their Binary/User defaulted; options carries the resolved heartbeat configuration for this specific destination. The returned string is written atomically to disk by melody:cron:generate. Any error (incl. ValidateNoForbiddenChars with your template's own forbidden-char list) aborts the generation before any file is touched.

Built-in templates

cron.BuiltinTemplates() returns the list of templates shipped with the integration (currently only *CrontabTemplate{} under the name cron.TemplateNameCrontab == "crontab"). NewGenerateCommand iterates this slice on construction, so you never register the built-ins by hand — they are always available even after you add your own.

Registering a custom template
import (
    melodycron "github.com/precision-soft/melody/integrations/cron/v3"
)

type KubernetesCronjobTemplate struct {
    Namespace string
    Image     string
}

func (instance *KubernetesCronjobTemplate) Name() string {
    return "k8s_cronjob"
}

func (instance *KubernetesCronjobTemplate) Render(entries []melodycron.Entry, options melodycron.RenderOptions) (string, error) {
    forbidden := []melodycron.ForbiddenChar{
        {Char: '\t', Reason: "tabs break YAML indentation"},
    }
    for _, entry := range entries {
        if validationErr := melodycron.ValidateNoForbiddenChars(entry.Command, forbidden, "k8s entry "+entry.Name); nil != validationErr {
            return "", validationErr
        }
    }
    // ... build the YAML from entries + instance.Namespace + instance.Image ...
    return yamlContent, nil
}

var _ melodycron.Template = (*KubernetesCronjobTemplate)(nil)

Hand the instance to GenerateCommand.RegisterTemplate before the kernel runs melody:cron:generate:

func (instance *Module) RegisterCliCommands(kernelInstance melodykernelcontract.Kernel) []melodyclicontract.Command {
    commands := []melodyclicontract.Command{
        command.NewAppInfoCommand(),
        command.NewProductListCommand(),
    }

    cronConfiguration := melodycron.NewConfiguration().
        Schedule(melodycron.CommandName(command.NewProductListCommand), &melodycron.EntryConfig{
            Schedule: &melodycron.Schedule{Minute: "0", Hour: "3"},
        })

    generateCommand := melodycron.NewGenerateCommand(cronConfiguration)
    generateCommand.RegisterTemplate(&KubernetesCronjobTemplate{
        Namespace: "production",
        Image:     "myapp:latest",
    })

    return append(commands, generateCommand)
}
Selecting the active template

Three sources, in priority order:

  1. --template=<name> on the CLI (explicit, wins over everything).
  2. melody.cron.template container parameter (set by RegisterDefaultParameters to "crontab", override via env-driven parameters or your own RegisterParameter call).
  3. Fallback: "crontab" (cron.TemplateNameCrontab).

If the resolved name has no template registered, melody:cron:generate errors with a message listing the names that are registered, so a typo in melody.cron.template = "k8s-cronjob" (dash instead of underscore) surfaces immediately.

Template-specific configuration

Each template owns its config shape via struct fields (Namespace, Image in the example above). Userland reads whatever it needs from melody parameters / env / config files at bootstrap and injects the values when constructing the template instance. The cron integration does not mediate template-specific config — it only resolves the active template name. This keeps each template self-contained and avoids leaking unrelated knobs into cron's parameter namespace.

Usage

1. Write a plain Melody CLI command
package command

import (
    melodyclicontract "github.com/precision-soft/melody/v3/cli/contract"
    melodyruntimecontract "github.com/precision-soft/melody/v3/runtime/contract"
)

type ProductListCommand struct{}

func NewProductListCommand() *ProductListCommand {
    return &ProductListCommand{}
}

func (instance *ProductListCommand) Name() string {
    return "product:list"
}

func (instance *ProductListCommand) Description() string {
    return "prints products in a table"
}

func (instance *ProductListCommand) Flags() []melodyclicontract.Flag {
    return []melodyclicontract.Flag{}
}

func (instance *ProductListCommand) Run(runtimeInstance melodyruntimecontract.Runtime, commandContext *melodyclicontract.CommandContext) error {
    return nil
}

var _ melodyclicontract.Command = (*ProductListCommand)(nil)

The command is a plain Melody CLI command — there is no cron-specific interface to implement. Scheduling lives in the configuration registry below.

2. Build the cron.Configuration registry and wire the generator
import (
    melodyclicontract "github.com/precision-soft/melody/v3/cli/contract"
    melodycron "github.com/precision-soft/melody/integrations/cron/v3"
    melodykernelcontract "github.com/precision-soft/melody/v3/kernel/contract"
)

func (instance *Module) RegisterCliCommands(kernelInstance melodykernelcontract.Kernel) []melodyclicontract.Command {
    commands := []melodyclicontract.Command{
        command.NewAppInfoCommand(),
        command.NewProductListCommand(),
    }

    cronConfiguration := melodycron.NewConfiguration().
        Schedule(melodycron.CommandName(command.NewProductListCommand), &melodycron.EntryConfig{
            Schedule: &melodycron.Schedule{Minute: "0", Hour: "3"},
        }).
        Schedule(melodycron.CommandName(command.NewAppInfoCommand), &melodycron.EntryConfig{
            Schedule: &melodycron.Schedule{Minute: "0", Hour: "12"},
        })

    return append(commands, melodycron.NewGenerateCommand(cronConfiguration))
}

cron.CommandName(NewProductListCommand) instantiates the constructor once and returns command.Name(), so the schedule references the command by constructor instead of a hardcoded string. The Configuration preserves insertion order — entries appear in the crontab in registration order.

Full reference module

A copy-pasteable end-to-end module wiring — scheduled command, custom KubernetesCronjobTemplate, RegisterParameters + RegisterCliCommands with template registration — lives in .example/module.go (and the matching files under v2/.example/ / v3/.example/ for the other bindings). The directory is dot-prefixed so go build ./... and go test ./... skip it; build it explicitly with go build ./.example/.

3. Generate the crontab file
./app melody:cron:generate \
    --out=generated_conf/cron/crontab \
    --logs-dir=var/log/cron \
    --user=www-data \
    --heartbeat-path=var/log/cron/heartbeat.crontab

Producing:

#############################################################################
#
# GENERATED FILE
# DO NOT EDIT LOCALLY
#
#############################################################################
0 3 * * * www-data /abs/path/app product:list >> '/abs/path/var/log/cron/product-list.log' 2>&1

* * * * * www-data /bin/touch /abs/path/var/log/cron/heartbeat.crontab
#############################################################################

When no command declares a schedule and --heartbeat-path (after the cascade) is empty, the command prints nothing to write and exits without creating the output file.

Footguns & caveats

  • The cron command has no hardcoded fallback values. Without either a CLI flag or a container parameter, melody:cron:generate errors out at run time with a message naming the missing flag/parameter. Use RegisterDefaultParameters for sensible defaults, or wire each parameter explicitly.
  • Render(...) does not silently default empty users. Every Entry must carry a non-empty User, and a non-empty Binary or non-empty Command. HeartbeatPath and HeartbeatCommand both require a HeartbeatUser. melody:cron:generate resolves the user via the cascade before calling Render, but anyone calling Render directly is responsible for the same.
  • Schedule.Expression() auto-fills empty fields with * wildcards without mutating the receiver. Use Schedule.Defaults() if you also want the struct fields populated in place (nil-safe).
  • Schedule.Defaults() mutates the receiver (returning the same pointer with empty fields replaced by *); the name is "Defaults" in the sense of "apply defaults", not "return a copy with defaults". If you need an unchanged original, copy the struct before the call.
  • Entries appear in registration order, i.e. the order of Configuration.Schedule(...) calls. Re-ordering the builder calls reshuffles the generated crontab.
  • Destination files are written in lexicographic order; each destination file is written atomically (temp file + rename) so crond never observes a truncated crontab even if the generator is killed mid-write.
  • The heartbeat line is appended to every destination file that gets written, unless restricted with --heartbeat-destination (see Heartbeat per-destination targeting).
  • The per-command log file name defaults to <sanitized-command-name>.log where : and / are replaced by -. Override per command with EntryConfig.LogFileName, opt out of : sanitization with EntryConfig.LogFileNameRaw = true, or disable logging entirely with EntryConfig.LogDisabled = true.
  • EntryConfig.LogFileName is joined with --logs-dir and rejected if the result escapes that directory (e.g. "../escape.log"), mirroring the EntryConfig.DestinationFile guard. Use a file name (or relative path) that stays inside the configured logs dir.
  • Multi-instance log file names preserve compound extensions: EntryConfig.LogFileName = "archive.tar.gz" with Instances = 2 yields archive-1.tar.gz and archive-2.tar.gz (not archive.tar-1.gz).
  • EntryConfig.Instances is intended for the default <binary> <command-name> shape — it appends --max-instances / --instance-index flags to your binary's arg list. When you set EntryConfig.Command with custom argv, those flags are not injected (the generator still emits N entries, each with the same argv and a per-instance log file); inject the flags yourself or build N distinct commands. Values < 1 (zero or negative) are normalized to 1.
  • EntryConfig.DestinationFile accepts absolute paths verbatim — the dir(--out) escape check applies only to relative values. An absolute DestinationFile of e.g. /etc/cron.d/billing is honored as-is, so the generator can write outside the default output directory when a command genuinely needs it. Relative paths are joined with dir(--out) and rejected if they escape that directory (e.g. "../escape").
  • Generated crontab files are written with mode 0644 and their parent directories are created with mode 0755. Both modes are intentionally non-configurable; if your target requires stricter permissions, run chmod from your deploy script after melody:cron:generate returns.
  • The resolved --logs-dir (or melody.cron.logs_dir) is auto-created with mode 0755 at generate time via os.MkdirAll — you do not need a separate mkdir -p step before crond runs the entries. The mkdir is skipped only when logsDir is empty (heartbeat-only or fully LogDisabled schedules); other failures (e.g. parent path is a file, permission denied) error out with cron: could not create the logs directory. The created directory's owner/group default to the user running melody:cron:generate — if your cron jobs run as a different system user (apache, www-data, …), run chown/chgrp from your deploy script so the entries can write their log files.
  • A blank Schedule value (every field empty) renders as * * * * *. That is intentional but rarely the right call — always set at least Minute.
  • The renderer applies POSIX shell quoting per token for EntryConfig.Command, entry.Binary, entry.Args, RenderOptions.HeartbeatCommand, LogPath, and HeartbeatPath — tokens containing spaces, quotes, or other shell metacharacters are single-quoted (with embedded ' escaped as '\''). Tokens with only safe characters are emitted unchanged. Never pre-quote a token yourself or wrap a whole binary arg1 arg2 string in quotes; the per-token quoting is enough, and pre-quoting turns the whole sequence into a single filename to crond. %, \n, and \r are rejected in any token because they have special meaning to the crontab daemon itself (line continuation / line termination) regardless of quoting; remove them at the source.
  • User fields (EntryConfig.User, the resolved --user cascade, RenderOptions.HeartbeatUser) are validated against embedded whitespace (space, tab, CR, LF) before rendering. A username with whitespace would split the crontab line apart; the generator rejects it with cron: ... contains whitespace; user fields must be single tokens. Usernames already come from trusted application code in practice — this is a defense-in-depth check.
  • The EntryConfig.LogFileName / EntryConfig.DestinationFile containment check is lexical: it rejects relative paths whose joined result has a .. segment escaping the parent (--logs-dir or dir(--out)). Symlinks are not resolved — the threat model assumes these values come from trusted application code, so the only case guarded against is a literal .. escape attempt.

Package surface

The three bindings expose the same identifiers. From any of github.com/precision-soft/melody/integrations/cron, .../cron/v2, or .../cron/v3:

  • Types: Schedule, EntryConfig, Configuration, ScheduledCommand, Entry, RenderOptions, GenerateCommand, Template, CrontabTemplate, ParameterRegistrar, ForbiddenChar.
  • Constructors / helpers: NewConfiguration, NewGenerateCommand, CommandName, Render, BuiltinTemplates, ValidateNoForbiddenChars, RegisterDefaultParameters.
  • Parameter-name constants: ParameterUser, ParameterLogsDir, ParameterBinary, ParameterDestinationFile, ParameterHeartbeatPath, ParameterHeartbeatAutoEnabled, ParameterTemplate.
  • Template-name constants: TemplateNameCrontab.
  • Globals: CrontabForbiddenChars.

Documentation

Index

Constants

View Source
const (
	ParameterUser                 = "melody.cron.user"
	ParameterLogsDir              = "melody.cron.logs_dir"
	ParameterBinary               = "melody.cron.binary"
	ParameterDestinationFile      = "melody.cron.destination_file"
	ParameterHeartbeatPath        = "melody.cron.heartbeat_path"
	ParameterHeartbeatAutoEnabled = "melody.cron.heartbeat.enabled"
	ParameterTemplate             = "melody.cron.template"
)
View Source
const TemplateNameCrontab = "crontab"

Variables

View Source
var (
	ErrNoOutputPath                       = errors.New("cron: no output path configured")
	ErrNoLogsDir                          = errors.New("cron: no logs directory configured")
	ErrTemplateNotFound                   = errors.New("cron: template not registered")
	ErrHeartbeatUserMissing               = errors.New("cron: heartbeat is configured but no user is set")
	ErrHeartbeatDestinationUnmatched      = errors.New("cron: heartbeat destination does not match any written destination")
	ErrHeartbeatDestinationDefaultMissing = errors.New("cron: heartbeat destination 'default' requested but the default destination has no entries")
	ErrDestinationEscape                  = errors.New("cron: path escapes the allowed directory")
	ErrEntryEmptyUser                     = errors.New("cron: entry has empty user")
	ErrEntryEmptyCommand                  = errors.New("cron: entry has no command to run")
	ErrForbiddenCharacter                 = errors.New("cron: token contains forbidden character")
	ErrFieldContainsWhitespace            = errors.New("cron: field contains whitespace")
)
View Source
var CrontabForbiddenChars = []ForbiddenChar{
	{Char: '%', Reason: "reserved by crontab as a line-continuation character (translated to a newline before the shell sees it); remove it at the source"},
	{Char: '\n', Reason: "terminates the crontab line; a literal newline inside a token splits one entry into multiple invalid lines"},
	{Char: '\r', Reason: "terminates the crontab line on many cron daemons; remove it before passing the token to the generator"},
}

Functions

func CommandName

func CommandName[T clicontract.Command](factory func() T) string

func RegisterDefaultParameters

func RegisterDefaultParameters(registrar ParameterRegistrar)

func Render

func Render(entries []Entry, options RenderOptions) (string, error)

func ValidateNoForbiddenChars

func ValidateNoForbiddenChars(tokens []string, forbidden []ForbiddenChar, context string) error

Types

type Configuration

type Configuration struct {
	// contains filtered or unexported fields
}

func NewConfiguration

func NewConfiguration() *Configuration

func (*Configuration) Entries

func (instance *Configuration) Entries() []*ScheduledCommand

func (*Configuration) Schedule

func (instance *Configuration) Schedule(commandName string, config *EntryConfig) *Configuration

type CrontabTemplate

type CrontabTemplate struct{}

func (*CrontabTemplate) Name

func (instance *CrontabTemplate) Name() string

func (*CrontabTemplate) Render

func (instance *CrontabTemplate) Render(entries []Entry, options RenderOptions) (string, error)

type Entry

type Entry struct {
	Name            string
	User            string
	Binary          string
	Args            []string
	Schedule        *Schedule
	Command         []string
	LogPath         string
	DestinationFile string
}

type EntryConfig

type EntryConfig struct {
	Schedule        *Schedule
	User            string
	LogFileName     string
	LogFileNameRaw  bool
	LogDisabled     bool
	DestinationFile string
	Command         []string
	Instances       int
}

type ForbiddenChar

type ForbiddenChar struct {
	Char   rune
	Reason string
}

type GenerateCommand

type GenerateCommand struct {
	// contains filtered or unexported fields
}

func NewGenerateCommand

func NewGenerateCommand(configuration *Configuration) *GenerateCommand

func (*GenerateCommand) Description

func (instance *GenerateCommand) Description() string

func (*GenerateCommand) Flags

func (instance *GenerateCommand) Flags() []clicontract.Flag

func (*GenerateCommand) Name

func (instance *GenerateCommand) Name() string

func (*GenerateCommand) RegisterTemplate

func (instance *GenerateCommand) RegisterTemplate(template Template)

func (*GenerateCommand) Run

func (instance *GenerateCommand) Run(
	runtimeInstance runtimecontract.Runtime,
	commandContext *clicontract.CommandContext,
) error

type Module added in v1.2.0

type Module struct {
	// contains filtered or unexported fields
}

func NewModule added in v1.2.0

func NewModule(config ModuleConfig) *Module

func (*Module) Description added in v1.2.0

func (instance *Module) Description() string

func (*Module) Name added in v1.2.0

func (instance *Module) Name() string

func (*Module) RegisterCliCommands added in v1.2.0

func (instance *Module) RegisterCliCommands(kernelInstance kernelcontract.Kernel) []clicontract.Command

func (*Module) RegisterParameters added in v1.2.0

func (instance *Module) RegisterParameters(registrar applicationcontract.ParameterRegistrar)

type ModuleConfig added in v1.2.0

type ModuleConfig struct {
	Configuration         *Configuration
	ConfigurationFactory  func(kernelInstance kernelcontract.Kernel) *Configuration
	WithDefaultParameters bool
}

type ParameterRegistrar

type ParameterRegistrar interface {
	RegisterParameter(name string, value any)
}

type RenderOptions

type RenderOptions struct {
	HeartbeatUser    string
	HeartbeatPath    string
	HeartbeatCommand []string
}

type Schedule

type Schedule struct {
	Minute     string
	Hour       string
	DayOfMonth string
	Month      string
	DayOfWeek  string
}

func (*Schedule) Defaults

func (instance *Schedule) Defaults() *Schedule

func (*Schedule) Expression

func (instance *Schedule) Expression() string

type ScheduledCommand

type ScheduledCommand struct {
	CommandName string
	Config      *EntryConfig
}

type Template

type Template interface {
	Name() string
	Render(entries []Entry, options RenderOptions) (string, error)
}

func BuiltinTemplates

func BuiltinTemplates() []Template

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL