middlewares

package
v0.25.1 Latest Latest
Warning

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

Go to latest
Published: May 16, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultPresetName = "json-post"

DefaultPresetName is the name of the bundled preset used as the documented fallback when [global] webhook-default-preset is unset. Returned by (*WebhookGlobalConfig).EffectiveDefaultPreset() when DefaultPreset is nil. The matching preset YAML lives at middlewares/presets/json-post.yaml and is embedded into the binary. See https://github.com/netresearch/ofelia/issues/676.

Variables

View Source
var (
	ErrPresetEmpty        = errors.New("preset specification cannot be empty")
	ErrPresetNotFound     = errors.New("preset not found")
	ErrRemoteDisabled     = errors.New("remote presets are disabled")
	ErrUntrustedSource    = errors.New("preset source not in trusted sources")
	ErrPresetFetchFailed  = errors.New("failed to fetch preset")
	ErrPresetTooLarge     = errors.New("preset file too large")
	ErrPresetInvalid      = errors.New("preset must have either url_scheme or body defined")
	ErrUnreplacedVars     = errors.New("URL contains unreplaced variables")
	ErrCacheExpired       = errors.New("cache expired")
	ErrCacheCollision     = errors.New("cache key collision")
	ErrNotGitHubShorthand = errors.New("not a GitHub shorthand")
	ErrInvalidGitHub      = errors.New("invalid GitHub shorthand format")
)

Preset errors

View Source
var (
	ErrWebhookNameEmpty   = errors.New("webhook name cannot be empty")
	ErrWebhookNotFound    = errors.New("webhook not found")
	ErrMissingVariable    = errors.New("required variable not provided")
	ErrWebhookHTTPFailed  = errors.New("webhook HTTP request failed")
	ErrMissingPresetOrURL = errors.New("either preset or url must be specified")
	ErrInvalidTrigger     = errors.New("invalid trigger type")
	ErrNegativeTimeout    = errors.New("timeout cannot be negative")
	ErrNegativeRetryCount = errors.New("retry-count cannot be negative")
	ErrNegativeRetryDelay = errors.New("retry-delay cannot be negative")
)

Webhook errors

View Source
var (
	ErrInvalidURLScheme = errors.New("URL scheme must be http or https")
	ErrMissingHost      = errors.New("URL must have a host")
	ErrMissingHostname  = errors.New("URL must have a hostname")
	ErrHostNotAllowed   = errors.New("host is not in allowed hosts list")
)

Webhook security errors

View Source
var (
	ErrDangerousPattern = errors.New("invalid path: contains dangerous pattern")
	ErrSystemDirectory  = errors.New("invalid path: cannot write to system directory")
)

Sanitize errors

View Source
var DefaultSanitizer = NewPathSanitizer()

Default sanitizer instance

View Source
var ErrInvalidSMTPTLSPolicy = errors.New("invalid smtp-tls-policy")

ErrInvalidSMTPTLSPolicy is the sentinel returned by Validate when an unknown `smtp-tls-policy` value is encountered. Production callers that want a hard-fail on misconfiguration (rather than the soft-fail-with-warn of resolveSMTPTLSPolicy) can branch on this via errors.Is.

resolveSMTPTLSPolicy intentionally does NOT return an error — it normalizes unknown values to the safe default (mandatory) and emits an slog.Warn so a typo cannot weaken transport security at runtime.

View Source
var TransportFactory = func() *http.Transport {
	fn := getTransportFactory()
	return fn()
}

TransportFactory creates HTTP transports for webhook requests (thread-safe access)

View Source
var ValidateWebhookURL = func(rawURL string) error {
	fn := getValidateWebhookURL()
	return fn(rawURL)
}

ValidateWebhookURL validates a URL for webhook requests (thread-safe access)

View Source
var Version = "dev"

Version is set during build and used in webhook templates

Functions

func ExtractVersionFromShorthand added in v0.16.0

func ExtractVersionFromShorthand(shorthand string) string

ExtractVersionFromShorthand extracts the version from a shorthand

func FormatGitHubShorthand added in v0.16.0

func FormatGitHubShorthand(org, repo, path, version string) string

FormatGitHubShorthand creates a shorthand string from components

func InitNotificationDedup added in v0.16.0

func InitNotificationDedup(cooldown time.Duration)

InitNotificationDedup initializes the global deduplicator with the specified cooldown period. Call this during configuration loading.

func IsBranch added in v0.16.0

func IsBranch(version string) bool

IsBranch attempts to determine if a version is a branch name

func IsEmpty

func IsEmpty(i any) bool

func IsGitHubShorthand added in v0.16.0

func IsGitHubShorthand(s string) bool

IsGitHubShorthand checks if a string is a GitHub shorthand

func IsSemanticVersion added in v0.16.0

func IsSemanticVersion(version string) bool

IsSemanticVersion checks if a version string looks like a semantic version

func IsVersioned added in v0.16.0

func IsVersioned(shorthand string) bool

IsVersioned checks if the shorthand includes an explicit version

func NewConfigurableTransport added in v0.17.0

func NewConfigurableTransport(config *WebhookSecurityConfig) *http.Transport

NewConfigurableTransport creates a standard HTTP transport. Security validation is handled by WebhookSecurityValidator before requests are made. The transport itself doesn't need additional restrictions since we follow the "trust the config" model - if users can run arbitrary commands, they can send webhooks to any configured destination.

func NewMail

func NewMail(c *MailConfig) core.Middleware

NewMail returns a Mail middleware if the given configuration is not empty

func NewOverlap

func NewOverlap(c *OverlapConfig) core.Middleware

NewOverlap returns a Overlap middleware if the given configuration is not empty

func NewSafeTransport added in v0.16.0

func NewSafeTransport() *http.Transport

NewSafeTransport creates a standard HTTP transport. URL validation is handled by the security validator before requests are made.

func NewSave

func NewSave(c *SaveConfig) core.Middleware

NewSave returns a Save middleware if the given configuration is not empty

func NewSlack deprecated

func NewSlack(c *SlackConfig) core.Middleware

NewSlack returns a Slack middleware if the given configuration is not empty

Deprecated: The Slack middleware is deprecated and will be removed in v1.0.0. Please migrate to the generic webhook notification system with the "slack" preset:

[webhook "slack-alerts"]
preset = slack
id = T00000000/B00000000
secret = XXXXXXXXXXXXXXXXXXXXXXXX
trigger = error

The new webhook system provides retry logic, multiple webhooks, and support for other services (Discord, Teams, ntfy, Pushover, PagerDuty, Gotify, etc.)

func NewWebhook added in v0.16.0

func NewWebhook(config *WebhookConfig, loader *PresetLoader) (core.Middleware, error)

NewWebhook creates a new Webhook middleware from configuration. Returns (nil, nil) when config is nil, indicating no middleware should be created.

func NewWebhookMiddleware added in v0.16.0

func NewWebhookMiddleware(webhooks []core.Middleware) core.Middleware

NewWebhookMiddleware creates a composite middleware from multiple webhook middlewares. Returns nil for an empty slice and the single webhook directly when there is only one — the composite is only needed to bypass the core.middlewareContainer.Use() type dedup that strikes when a second *Webhook joins the same chain.

func ParseGitHubShorthand added in v0.16.0

func ParseGitHubShorthand(shorthand string) (string, error)

ParseGitHubShorthand parses a GitHub shorthand URL and returns the raw URL Format: gh:org/repo/path/to/file.yaml@version Examples:

  • gh:netresearch/ofelia-presets/slack.yaml
  • gh:netresearch/ofelia-presets/notifications/slack.yaml@v1.0.0
  • gh:myorg/my-presets/custom@main

func ParseWebhookNames added in v0.16.0

func ParseWebhookNames(s string) []string

ParseWebhookNames parses a comma-separated list of webhook names

func RestoreHistory added in v0.18.0

func RestoreHistory(saveFolder string, maxAge time.Duration, jobs []core.Job, logger *slog.Logger) error

RestoreHistory restores job history from saved JSON files in the save folder. It populates the in-memory history of jobs that support SetLastRun. Only files newer than maxAge are restored.

func SanitizeFilename added in v0.10.1

func SanitizeFilename(filename string) string

SanitizeFilename is a convenience function using the default sanitizer

func SanitizeJobName added in v0.10.1

func SanitizeJobName(jobName string) string

SanitizeJobName is a convenience function using the default sanitizer

func SanitizePath added in v0.10.1

func SanitizePath(path string) string

SanitizePath is a convenience function using the default sanitizer

func SetGlobalSecurityConfig added in v0.17.0

func SetGlobalSecurityConfig(config *WebhookSecurityConfig)

SetGlobalSecurityConfig sets the global security configuration for webhooks This should be called during initialization with the parsed configuration.

Emits a single startup-time slog.Warn when the resolved AllowedHosts collapses to ["*"] (whether explicitly configured or by default). A typo in the `webhook-allowed-hosts` INI key would otherwise yield wide-open egress with no operator-visible signal that the allow-list they thought they had configured is actually empty. Tracked in https://github.com/netresearch/ofelia/issues/653.

Passing nil restores the package defaults silently — used by tests and reload paths that revert state. The warning is reserved for operator-meaningful startup state.

func SetTransportFactoryForTest added in v0.17.0

func SetTransportFactoryForTest(fn func() *http.Transport)

SetTransportFactoryForTest allows tests to override the transport factory (thread-safe)

func SetValidateWebhookURLForTest added in v0.17.0

func SetValidateWebhookURLForTest(fn func(string) error)

SetValidateWebhookURLForTest allows tests to override the URL validator (thread-safe)

func StripVersionFromShorthand added in v0.16.0

func StripVersionFromShorthand(shorthand string) string

StripVersionFromShorthand removes the version from a shorthand

func ValidateGitHubShorthand added in v0.16.0

func ValidateGitHubShorthand(shorthand string) error

ValidateGitHubShorthand validates that a GitHub shorthand is well-formed

func ValidateWebhookURLImpl added in v0.16.0

func ValidateWebhookURLImpl(rawURL string) error

ValidateWebhookURLImpl validates basic URL requirements. This is the default validator that allows all hosts (consistent with local command trust model). For whitelist mode, use WebhookSecurityValidator with specific AllowedHosts.

Types

type CacheStats added in v0.16.0

type CacheStats struct {
	MemoryEntries int
	DiskEntries   int
}

CacheStats holds cache statistics

type GitHubShorthand added in v0.16.0

type GitHubShorthand struct {
	Org     string
	Repo    string
	Path    string
	Version string // Can be tag, branch, or commit
}

GitHubShorthand represents a parsed GitHub shorthand URL

func ParseGitHubShorthandDetails added in v0.16.0

func ParseGitHubShorthandDetails(shorthand string) (*GitHubShorthand, error)

ParseGitHubShorthandDetails parses and returns the structured details

type Mail

type Mail struct {
	MailConfig
}

Mail middleware delivers a email just after an execution finishes

func (*Mail) ContinueOnStop

func (m *Mail) ContinueOnStop() bool

ContinueOnStop always returns true; we always want to report the final status

func (*Mail) Run

func (m *Mail) Run(ctx *core.Context) error

Run sends an email with the result of the execution

type MailConfig

type MailConfig struct {
	SMTPHost          string `gcfg:"smtp-host" mapstructure:"smtp-host"`
	SMTPPort          int    `gcfg:"smtp-port" mapstructure:"smtp-port"`
	SMTPUser          string `gcfg:"smtp-user" mapstructure:"smtp-user" json:"-"`
	SMTPPassword      string `gcfg:"smtp-password" mapstructure:"smtp-password" json:"-"`
	SMTPTLSSkipVerify bool   `gcfg:"smtp-tls-skip-verify" mapstructure:"smtp-tls-skip-verify"`
	// SMTPTLSPolicy controls STARTTLS behavior. See SMTPTLSPolicy for valid
	// values and the security rationale for the mandatory-by-default change.
	// Empty string is treated as "mandatory".
	SMTPTLSPolicy   SMTPTLSPolicy `gcfg:"smtp-tls-policy" mapstructure:"smtp-tls-policy"`
	EmailTo         string        `gcfg:"email-to" mapstructure:"email-to"`
	EmailFrom       string        `gcfg:"email-from" mapstructure:"email-from"`
	EmailSubject    string        `gcfg:"email-subject" mapstructure:"email-subject"`
	MailOnlyOnError *bool         `gcfg:"mail-only-on-error" mapstructure:"mail-only-on-error"`
	// Dedup is the notification deduplicator (set by config loader, not INI)
	Dedup *NotificationDedup `mapstructure:"-" json:"-"`
	// contains filtered or unexported fields
}

MailConfig configuration for the Mail middleware

type NotificationDedup added in v0.16.0

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

NotificationDedup provides deduplication of error notifications. It tracks recent error notifications and suppresses duplicates within a configurable cooldown period to prevent notification spam.

var DefaultNotificationDedup *NotificationDedup

DefaultNotificationDedup is the global deduplicator instance used by notification middlewares. It's initialized when configuration is loaded.

func NewNotificationDedup added in v0.16.0

func NewNotificationDedup(cooldown time.Duration) *NotificationDedup

NewNotificationDedup creates a new notification deduplicator with the specified cooldown period. If cooldown is 0, deduplication is disabled and all notifications are allowed.

func (*NotificationDedup) Cleanup added in v0.16.0

func (d *NotificationDedup) Cleanup()

Cleanup removes expired entries from the deduplication map. This should be called periodically to prevent memory leaks for jobs that no longer fail.

func (*NotificationDedup) Len added in v0.16.0

func (d *NotificationDedup) Len() int

Len returns the number of entries in the deduplication map. Useful for testing and monitoring.

func (*NotificationDedup) ShouldNotify added in v0.16.0

func (d *NotificationDedup) ShouldNotify(ctx *core.Context) bool

ShouldNotify returns true if the notification should be sent, false if it should be suppressed as a duplicate. Successful executions always return true (no deduplication for success). Failed executions are deduplicated based on job name, command, and error message.

func (*NotificationDedup) StartCleanupRoutine added in v0.16.0

func (d *NotificationDedup) StartCleanupRoutine(interval time.Duration) func()

StartCleanupRoutine starts a background goroutine that periodically cleans up expired entries. Returns a stop function to cancel the routine.

type Overlap

type Overlap struct {
	OverlapConfig
}

Overlap when this middleware is enabled avoid to overlap executions from a specific job

func (*Overlap) ContinueOnStop

func (m *Overlap) ContinueOnStop() bool

ContinueOnStop Overlap is only called if the process is still running

func (*Overlap) Run

func (m *Overlap) Run(ctx *core.Context) error

Run stops the execution if the another execution is already running

type OverlapConfig

type OverlapConfig struct {
	NoOverlap bool `gcfg:"no-overlap" mapstructure:"no-overlap"`
}

OverlapConfig configuration for the Overlap middleware

type PathSanitizer added in v0.10.1

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

PathSanitizer provides secure path sanitization utilities

func NewPathSanitizer added in v0.10.1

func NewPathSanitizer() *PathSanitizer

NewPathSanitizer creates a new path sanitizer with security rules

func (*PathSanitizer) SanitizeFilename added in v0.10.1

func (ps *PathSanitizer) SanitizeFilename(filename string) string

SanitizeFilename sanitizes a filename for safe file system operations

func (*PathSanitizer) SanitizeJobName added in v0.10.1

func (ps *PathSanitizer) SanitizeJobName(jobName string) string

SanitizeJobName sanitizes a job name for use in filenames

func (*PathSanitizer) SanitizePath added in v0.10.1

func (ps *PathSanitizer) SanitizePath(path string) string

SanitizePath sanitizes a path to prevent directory traversal and injection

func (*PathSanitizer) ValidateSaveFolder added in v0.10.1

func (ps *PathSanitizer) ValidateSaveFolder(folder string) error

ValidateSaveFolder validates that a save folder path is safe to use

type Preset added in v0.16.0

type Preset struct {
	// Metadata
	Name        string `yaml:"name"`
	Description string `yaml:"description"`
	Version     string `yaml:"version"`

	// URL configuration
	URLScheme string `yaml:"url_scheme"` //nolint:tagliatelle // snake_case is idiomatic for YAML configs

	// HTTP configuration
	Method  string            `yaml:"method"`
	Headers map[string]string `yaml:"headers"`

	// Variable definitions
	Variables map[string]PresetVariable `yaml:"variables"`

	// Body template (Go text/template format)
	Body string `yaml:"body"`
}

Preset defines a webhook notification preset

func ParsePreset added in v0.16.0

func ParsePreset(data []byte) (*Preset, error)

ParsePreset parses a preset from YAML data

func (*Preset) BuildURL added in v0.16.0

func (p *Preset) BuildURL(config *WebhookConfig) (string, error)

BuildURL constructs the final URL by substituting variables

func (*Preset) RenderBody added in v0.16.0

func (p *Preset) RenderBody(data *WebhookData) (string, error)

RenderBody renders the body template with the given data

func (*Preset) RenderBodyWithPreset added in v0.16.0

func (p *Preset) RenderBodyWithPreset(data map[string]any) (string, error)

RenderBodyWithPreset renders the body template with both webhook data and preset config

type PresetCache added in v0.16.0

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

PresetCache provides caching for remote presets

func NewPresetCache added in v0.16.0

func NewPresetCache(cacheDir string, ttl time.Duration) *PresetCache

NewPresetCache creates a new preset cache.

When cacheDir is empty, the default location comes from defaultPresetCacheDir (typically $XDG_CACHE_HOME/ofelia/presets on Linux, ~/Library/Caches/ofelia/presets on macOS) and is created with 0o700 perms — both for fresh directories (via os.MkdirAll) and for pre-existing ones (via an explicit os.Chmod, because os.MkdirAll does not adjust modes of existing entries).

When the caller supplies an explicit cacheDir, perms are left untouched if the directory already exists, and new directories are created with the previous 0o750 default. Operators who pass their own path are assumed to have set permissions deliberately.

func (*PresetCache) Cleanup added in v0.16.0

func (c *PresetCache) Cleanup() error

Cleanup removes expired entries from cache

func (*PresetCache) Clear added in v0.16.0

func (c *PresetCache) Clear() error

Clear removes all cached presets

func (*PresetCache) Get added in v0.16.0

func (c *PresetCache) Get(url string) (*Preset, error)

Get retrieves a preset from cache

func (*PresetCache) Invalidate added in v0.16.0

func (c *PresetCache) Invalidate(url string)

Invalidate removes a preset from cache

func (*PresetCache) Put added in v0.16.0

func (c *PresetCache) Put(url string, preset *Preset) error

Put stores a preset in cache

func (*PresetCache) Stats added in v0.16.0

func (c *PresetCache) Stats() CacheStats

Stats returns cache statistics

type PresetDataForTemplate added in v0.16.0

type PresetDataForTemplate struct {
	ID       string
	Secret   string
	URL      string
	Link     string
	LinkText string
}

PresetDataForTemplate provides preset config to templates that need it

type PresetLoader added in v0.16.0

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

PresetLoader handles loading presets from various sources

func NewPresetLoader added in v0.16.0

func NewPresetLoader(globalConfig *WebhookGlobalConfig) *PresetLoader

NewPresetLoader creates a new preset loader.

The HTTP client used for remote preset fetches (loadFromURL, loadFromGitHub) is constructed here from TransportFactory() and cached on the returned loader. Callers that need to influence the transport (e.g. tests using SetTransportFactoryForTest) must install the override BEFORE calling NewPresetLoader; replacing the factory afterwards does not affect the already-cached client.

func (*PresetLoader) AddLocalPresetDir added in v0.16.0

func (l *PresetLoader) AddLocalPresetDir(dir string)

AddLocalPresetDir adds a directory to search for local preset files

func (*PresetLoader) DefaultPreset added in v0.25.1

func (l *PresetLoader) DefaultPreset() string

DefaultPreset returns the effective global default preset name — (*WebhookGlobalConfig).EffectiveDefaultPreset() unwrapped, with a nil globalConfig also resolving to the bundled DefaultPresetName so tests that construct a loader without a global config still get the fallback. Callers fill this into WebhookConfig.Preset when the per-webhook value is empty, so url-only webhooks work without each one redeclaring `preset`.

Operators can opt out of the fallback by setting `webhook-default-preset` to an empty string in INI or via Docker label; that path returns "" here, and NewWebhook then fails attachment for any webhook that omits `preset` — regardless of whether `url` is set — with an error naming webhook-default-preset so operators can grep their way to the docs. (Setting `url` alone is not enough once the fallback is disabled: `url` only overrides the preset's url_scheme, not the preset itself.)

See https://github.com/netresearch/ofelia/issues/676.

func (*PresetLoader) GetBundledPreset added in v0.16.0

func (l *PresetLoader) GetBundledPreset(name string) (*Preset, bool)

GetBundledPreset returns a bundled preset by name

func (*PresetLoader) ListBundledPresets added in v0.16.0

func (l *PresetLoader) ListBundledPresets() []string

ListBundledPresets returns the names of all bundled presets

func (*PresetLoader) Load added in v0.16.0

func (l *PresetLoader) Load(presetSpec string) (*Preset, error)

Load loads a preset by name or path Supports: - Built-in preset names: "slack", "discord", etc. - Local file paths: "/path/to/preset.yaml", "./preset.yaml" - GitHub shorthand: "gh:org/repo/path/preset.yaml@v1.0" - Full URLs: "https://example.com/preset.yaml"

type PresetVariable added in v0.16.0

type PresetVariable struct {
	Description string `yaml:"description"`
	Required    bool   `yaml:"required"`
	Sensitive   bool   `yaml:"sensitive"`
	Default     string `yaml:"default,omitempty"`
	Example     string `yaml:"example,omitempty"`
}

PresetVariable defines a variable that can be used in the preset

type SMTPTLSPolicy added in v0.25.0

type SMTPTLSPolicy string

SMTPTLSPolicy controls the STARTTLS posture of the outbound SMTP dialer. It maps 1:1 onto go-mail's StartTLSPolicy enum and is exposed as a string in the INI config (`smtp-tls-policy`) so operators don't have to know the upstream library's integer values.

Default (empty string) resolves to MandatoryStartTLS — the upstream library's own recommendation for any modern SMTP server. The previous behavior (OpportunisticStartTLS) silently sent credentials and message body in cleartext when the server did not advertise STARTTLS, even when `smtp-tls-skip-verify` was off, which violated the operator's intent. See https://github.com/netresearch/ofelia/issues/653.

const (
	// SMTPTLSPolicyMandatory requires STARTTLS; the dialer aborts with an
	// error if the server does not advertise it. This is the default.
	SMTPTLSPolicyMandatory SMTPTLSPolicy = "mandatory"

	// SMTPTLSPolicyOpportunistic tries STARTTLS when offered but silently
	// falls back to plaintext if it is not. This is the upstream
	// go-mail/mail/v2 default and the previous Ofelia behavior. Use only
	// when sending to a legacy server that cannot offer STARTTLS but the
	// network path is otherwise trusted (e.g. localhost-only relay).
	SMTPTLSPolicyOpportunistic SMTPTLSPolicy = "opportunistic"

	// SMTPTLSPolicyNone disables STARTTLS entirely; messages and credentials
	// are sent in cleartext. Required for some test fixtures (MailHog,
	// emersion/go-smtp without TLS) and intentionally insecure.
	SMTPTLSPolicyNone SMTPTLSPolicy = "none"
)

SMTPTLSPolicy constants. The empty string is also accepted (and treated as `mandatory`) so operators upgrading do not have to touch their config.

func (SMTPTLSPolicy) Valid added in v0.25.0

func (p SMTPTLSPolicy) Valid() bool

Valid reports whether p is one of the documented values (or empty, meaning "use default"). Unknown values are rejected so config validation surfaces operator typos that would otherwise be silently normalized.

func (SMTPTLSPolicy) Validate added in v0.25.0

func (p SMTPTLSPolicy) Validate() error

Validate returns ErrInvalidSMTPTLSPolicy (wrapped with the offending value for diagnostics) when p is not one of the documented values. Returns nil for the empty string (treated as "mandatory") and the three documented constants. Callers that want a hard-fail on misconfiguration at config-load time should use this; callers that should never break existing deployments on a typo should use resolveSMTPTLSPolicy instead.

type Save

type Save struct {
	SaveConfig
}

Save the save middleware saves to disk a dump of the stdout and stderr after every execution of the process

func (*Save) ContinueOnStop

func (m *Save) ContinueOnStop() bool

ContinueOnStop always returns true; we always want to report the final status

func (*Save) Run

func (m *Save) Run(ctx *core.Context) error

Run save the result of the execution to disk

type SaveConfig

type SaveConfig struct {
	// SaveFolder is the directory path where job execution logs and metadata are saved.
	// When configured, execution output (stdout, stderr) and context (JSON) are saved
	// after each job run. Leave empty to disable saving.
	SaveFolder string `gcfg:"save-folder" mapstructure:"save-folder"`
	// SaveOnlyOnError when true, only saves execution logs when a job fails.
	// Defaults to false (saves all executions).
	SaveOnlyOnError *bool `gcfg:"save-only-on-error" mapstructure:"save-only-on-error"`
	// RestoreHistory controls whether previously saved execution history is restored on startup.
	// When nil (default), history restoration is enabled if SaveFolder is configured.
	// Set explicitly to false to disable restoration even when SaveFolder is set.
	RestoreHistory *bool `gcfg:"restore-history" mapstructure:"restore-history"`
	// RestoreHistoryMaxAge defines the maximum age of execution history to restore on startup.
	// Only executions newer than this duration are restored. Defaults to 24 hours.
	RestoreHistoryMaxAge time.Duration `gcfg:"restore-history-max-age" mapstructure:"restore-history-max-age"`
}

SaveConfig configuration for the Save middleware

func (*SaveConfig) GetRestoreHistoryMaxAge added in v0.18.0

func (c *SaveConfig) GetRestoreHistoryMaxAge() time.Duration

GetRestoreHistoryMaxAge returns the max age for history restoration. Defaults to 24 hours.

func (*SaveConfig) RestoreHistoryEnabled added in v0.18.0

func (c *SaveConfig) RestoreHistoryEnabled() bool

RestoreHistoryEnabled returns whether history restoration is enabled. Defaults to true when SaveFolder is configured.

type Slack

type Slack struct {
	SlackConfig
	Client *http.Client
}

Slack middleware calls to a Slack input-hook after every execution of a job

func (*Slack) ContinueOnStop

func (m *Slack) ContinueOnStop() bool

ContinueOnStop always returns true; we always want to report the final status

func (*Slack) Run

func (m *Slack) Run(ctx *core.Context) error

Run sends a message to the Slack channel and stops the execution to gather metrics

type SlackConfig

type SlackConfig struct {
	SlackWebhook     string `gcfg:"slack-webhook" mapstructure:"slack-webhook" json:"-"`
	SlackOnlyOnError *bool  `gcfg:"slack-only-on-error" mapstructure:"slack-only-on-error"`
	// Dedup is the notification deduplicator (set by config loader, not INI)
	Dedup *NotificationDedup `mapstructure:"-" json:"-"`
}

SlackConfig configuration for the Slack middleware

type TriggerType added in v0.16.0

type TriggerType string

TriggerType defines when a webhook notification should be sent

const (
	TriggerAlways  TriggerType = "always"  // Send on every execution
	TriggerError   TriggerType = "error"   // Send only on errors
	TriggerSuccess TriggerType = "success" // Send only on success
	TriggerSkipped TriggerType = "skipped" // Send only on skipped executions
)

type Webhook added in v0.16.0

type Webhook struct {
	Config       *WebhookConfig
	Preset       *Preset
	PresetLoader *PresetLoader
	Client       *http.Client
}

Webhook middleware sends HTTP webhook notifications after job execution

func (*Webhook) ContinueOnStop added in v0.16.0

func (w *Webhook) ContinueOnStop() bool

ContinueOnStop returns true because we want to report final execution status

func (*Webhook) Run added in v0.16.0

func (w *Webhook) Run(ctx *core.Context) error

Run executes the webhook notification

type WebhookConfig added in v0.16.0

type WebhookConfig struct {
	// Name is the unique identifier for this webhook (from INI section name)
	Name string `gcfg:"-" mapstructure:"-"`

	// Preset specifies the preset to use (e.g., "slack", "discord", "gh:org/repo/preset.yaml@v1.0")
	Preset string `gcfg:"preset" mapstructure:"preset"`

	// ID is a generic identifier used by the preset's URL scheme (e.g., Slack workspace/bot ID)
	ID string `gcfg:"id" mapstructure:"id" json:"-"`

	// Secret is a generic secret/token used by the preset's URL scheme
	Secret string `gcfg:"secret" mapstructure:"secret" json:"-"`

	// URL overrides the preset's url_scheme entirely (useful for custom endpoints)
	URL string `gcfg:"url" mapstructure:"url" json:"-"`

	// Link is an optional URL to include in notifications (e.g., link to logs, dashboard)
	Link string `gcfg:"link" mapstructure:"link"`

	// LinkText is the display text for the link (defaults to "View Details" if link is set)
	LinkText string `gcfg:"link-text" mapstructure:"link-text"`

	// Trigger determines when to send notifications
	Trigger TriggerType `gcfg:"trigger" mapstructure:"trigger"`

	// Timeout for the HTTP request
	Timeout time.Duration `gcfg:"timeout" mapstructure:"timeout"`

	// RetryCount is the number of retry attempts on failure
	RetryCount int `gcfg:"retry-count" mapstructure:"retry-count"`

	// RetryDelay is the delay between retry attempts
	RetryDelay time.Duration `gcfg:"retry-delay" mapstructure:"retry-delay"`

	// CustomVars holds additional custom variables for template expansion
	CustomVars map[string]string `gcfg:"-" mapstructure:"-"`

	// Dedup is the notification deduplicator (set by config loader, not INI)
	Dedup *NotificationDedup `mapstructure:"-" json:"-"`
}

WebhookConfig holds configuration for a single webhook endpoint

func DefaultWebhookConfig added in v0.16.0

func DefaultWebhookConfig() *WebhookConfig

DefaultWebhookConfig returns default webhook configuration values

func (*WebhookConfig) ApplyDefaults added in v0.16.0

func (c *WebhookConfig) ApplyDefaults()

ApplyDefaults applies default values to empty fields

func (*WebhookConfig) ShouldNotify added in v0.16.0

func (c *WebhookConfig) ShouldNotify(failed, skipped bool) bool

ShouldNotify determines if a notification should be sent based on trigger and execution state

func (*WebhookConfig) Validate added in v0.16.0

func (c *WebhookConfig) Validate() error

Validate checks the webhook configuration for errors

type WebhookData added in v0.16.0

type WebhookData struct {
	Job       WebhookJobData
	Execution WebhookExecutionData
	Host      WebhookHostData
	Ofelia    WebhookOfeliaData
}

WebhookData is the data structure passed to webhook templates

type WebhookExecutionData added in v0.16.0

type WebhookExecutionData struct {
	ID        string
	Status    string
	Failed    bool
	Skipped   bool
	Duration  time.Duration
	Error     string
	Output    string
	Stderr    string
	ExitCode  int
	StartTime time.Time
	EndTime   time.Time
}

WebhookExecutionData contains execution information for templates

type WebhookGlobalConfig added in v0.16.0

type WebhookGlobalConfig struct {
	// Webhooks is a comma-separated list of webhook names to use globally.
	// (Configured as `webhook-webhooks` in [global]. The per-job `webhooks = ...`
	// key lives on JobWebhookConfig and is separate.)
	Webhooks string `gcfg:"webhook-webhooks" mapstructure:"webhook-webhooks"`

	// AllowRemotePresets enables fetching presets from remote URLs
	AllowRemotePresets bool `gcfg:"webhook-allow-remote-presets" mapstructure:"webhook-allow-remote-presets"`

	// TrustedPresetSources is a comma-separated list of trusted remote preset sources
	// Supports glob patterns (e.g., "gh:netresearch/*", "gh:myorg/ofelia-presets/*")
	TrustedPresetSources string `gcfg:"webhook-trusted-preset-sources" mapstructure:"webhook-trusted-preset-sources"`

	// PresetCacheTTL is how long to cache remote presets
	PresetCacheTTL time.Duration `gcfg:"webhook-preset-cache-ttl" mapstructure:"webhook-preset-cache-ttl"`

	// PresetCacheDir is the directory for caching remote presets
	PresetCacheDir string `gcfg:"webhook-preset-cache-dir" mapstructure:"webhook-preset-cache-dir"`

	// AllowedHosts controls which hosts webhooks can target.
	// Default: "*" (allow all hosts) - consistent with local command execution trust model
	// Set to specific hosts for whitelist mode: "hooks.slack.com, ntfy.internal, 192.168.1.20"
	// Supports wildcards: "*.example.com"
	AllowedHosts string `gcfg:"webhook-allowed-hosts" mapstructure:"webhook-allowed-hosts"`

	// DefaultPreset is the preset name used when a per-webhook configuration
	// omits the `preset` field. Pointer-typed so we can distinguish three
	// operator intents (mirrors the SaveConfig.RestoreHistory pattern):
	//
	//   - nil → operator did not set the key at all; resolve at access time
	//     to DefaultPresetName ("json-post", the bundled JSON POST preset)
	//     via EffectiveDefaultPreset(). Lets a webhook with just `url = ...`
	//     work out of the box.
	//   - non-nil "" (empty string) → operator explicitly opted out of the
	//     fallback; webhooks missing `preset` fail attachment with a logged
	//     error.
	//   - non-nil non-empty → operator's chosen fallback (custom preset name).
	//
	// See https://github.com/netresearch/ofelia/issues/676.
	DefaultPreset *string `gcfg:"webhook-default-preset" mapstructure:"webhook-default-preset"`
}

WebhookGlobalConfig holds global webhook settings.

All keys are configured under the [global] section of the INI config file and use the `webhook-` prefix to avoid colliding with other [global] settings.

func DefaultWebhookGlobalConfig added in v0.16.0

func DefaultWebhookGlobalConfig() *WebhookGlobalConfig

DefaultWebhookGlobalConfig returns default global webhook configuration

func (*WebhookGlobalConfig) EffectiveDefaultPreset added in v0.25.1

func (g *WebhookGlobalConfig) EffectiveDefaultPreset() string

EffectiveDefaultPreset returns the preset name to use as the fallback when a per-webhook config omits `preset`. Resolves the three intents encoded on (*string) DefaultPreset:

  • nil → fall back to DefaultPresetName ("json-post").
  • non-nil "" → operator explicitly opted out — no fallback.
  • non-nil "X" → operator's chosen fallback name.

Called at webhook attach time (NewWebhook) rather than at startup, so late mutations to DefaultPreset via INI reload or label sync take effect on the next attach without restart. Mirrors the access-time resolution pattern of SaveConfig.RestoreHistoryEnabled.

type WebhookHostData added in v0.16.0

type WebhookHostData struct {
	Hostname  string
	Timestamp time.Time
}

WebhookHostData contains host information for templates

type WebhookJobData added in v0.16.0

type WebhookJobData struct {
	Name     string
	Command  string
	Schedule string
	Type     string
}

WebhookJobData contains job information for templates

type WebhookManager added in v0.16.0

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

WebhookManager manages multiple webhook configurations

func NewWebhookManager added in v0.16.0

func NewWebhookManager(globalConfig *WebhookGlobalConfig) *WebhookManager

NewWebhookManager creates a new webhook manager

func (*WebhookManager) Get added in v0.16.0

func (m *WebhookManager) Get(name string) (*WebhookConfig, bool)

Get returns a webhook configuration by name

func (*WebhookManager) GetGlobalMiddlewares added in v0.16.0

func (m *WebhookManager) GetGlobalMiddlewares() ([]core.Middleware, error)

GetGlobalMiddlewares returns middlewares for globally configured webhooks

func (*WebhookManager) GetMiddlewares added in v0.16.0

func (m *WebhookManager) GetMiddlewares(names []string) ([]core.Middleware, error)

GetMiddlewares returns middlewares for the specified webhook names

func (*WebhookManager) GlobalWebhookNames added in v0.25.1

func (m *WebhookManager) GlobalWebhookNames() []string

GlobalWebhookNames returns the parsed names of globally configured webhooks (the `[global] webhook-webhooks = ...` selector). Returns nil when no globals are configured.

Used by the per-job attach path so each job can union global + per-job webhook names into a single composite — propagating globals via the scheduler's middleware chain doesn't work because core.middlewareContainer would dedup the scheduler's *WebhookMiddleware against the job's own. See https://github.com/netresearch/ofelia/issues/670.

func (*WebhookManager) Register added in v0.16.0

func (m *WebhookManager) Register(config *WebhookConfig) error

Register adds a webhook configuration

type WebhookMiddleware added in v0.16.0

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

WebhookMiddleware is a composite middleware that dispatches to multiple webhooks.

A composite wrapper is required because core.middlewareContainer.Use() deduplicates middlewares by their reflect type — adding two *Webhook instances directly would silently drop the second one. See #670.

func (*WebhookMiddleware) ContinueOnStop added in v0.16.0

func (w *WebhookMiddleware) ContinueOnStop() bool

ContinueOnStop returns true because we want to report final status

func (*WebhookMiddleware) Run added in v0.16.0

func (w *WebhookMiddleware) Run(ctx *core.Context) error

Run dispatches to each inner webhook in order, after the chain below the composite (typically just the job itself) has executed.

Re-entrancy invariant: ctx.Next() runs the rest of the chain (and the job) then we call ctx.Stop(err) and loop calling each inner webhook.Run(ctx). Each inner Webhook.Run also calls ctx.Next() + ctx.Stop. This is safe NOT because the composite happens to be the last middleware, but because core.Context.doNext() short-circuits on !Execution.IsRunning before re-running the job (core/common.go:136), and ctx.Stop is idempotent under the same flag (core/common.go:157). The IsRunning gate — not composite position — enforces single-job-execution, so the invariant survives any future middleware appended after the composite. Be aware, however, that any post-composite middleware whose ContinueOnStop()==true will run N times (once per inner webhook) on the same Context.

Errors from individual webhook.Run calls are intentionally discarded — Webhook.Run returns the underlying *job* error (not a notification error) and logs its own delivery failures internally via ctx.Logger.Error. The outer return value preserves the job's error so callers up the middleware chain still see job failures.

func (*WebhookMiddleware) Webhooks added in v0.25.1

func (w *WebhookMiddleware) Webhooks() []core.Middleware

Webhooks returns a shallow copy of the inner webhook middlewares.

Exposed for tests that need to verify multi-webhook attachment without reaching into unexported fields. The returned slice header is copied so callers cannot append, reorder, or replace the composite's stored list — but each element aliases the composite's stored *Webhook, so callers must not mutate Webhook.Config (URL/Secret/Trigger) on the returned entries. In practice send() re-validates URL on every dispatch, so this is documentation-grade rather than security-grade.

type WebhookOfeliaData added in v0.16.0

type WebhookOfeliaData struct {
	Version string
}

WebhookOfeliaData contains Ofelia metadata for templates

type WebhookSecurityConfig added in v0.16.0

type WebhookSecurityConfig struct {
	// AllowedHosts controls which hosts webhooks can target.
	// "*" = allow all hosts (default, consistent with local command trust model)
	// Specific list = whitelist mode, only those hosts allowed
	// Supports wildcards: "*.example.com"
	AllowedHosts []string
}

WebhookSecurityConfig holds security configuration for webhooks

func DefaultWebhookSecurityConfig added in v0.16.0

func DefaultWebhookSecurityConfig() *WebhookSecurityConfig

DefaultWebhookSecurityConfig returns the default security configuration Default: AllowedHosts=["*"] for consistency with local command execution trust model

func SecurityConfigFromGlobal added in v0.17.0

func SecurityConfigFromGlobal(global *WebhookGlobalConfig) *WebhookSecurityConfig

SecurityConfigFromGlobal creates a WebhookSecurityConfig from WebhookGlobalConfig

type WebhookSecurityValidator added in v0.16.0

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

WebhookSecurityValidator validates URLs with configurable security rules

func NewWebhookSecurityValidator added in v0.16.0

func NewWebhookSecurityValidator(config *WebhookSecurityConfig) *WebhookSecurityValidator

NewWebhookSecurityValidator creates a new security validator

func (*WebhookSecurityValidator) Validate added in v0.16.0

func (v *WebhookSecurityValidator) Validate(rawURL string) error

Validate checks if a URL is safe to access based on the allowed hosts configuration. If AllowedHosts contains "*", all hosts are allowed (default behavior). Otherwise, only hosts in the whitelist are allowed.

Jump to

Keyboard shortcuts

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