Documentation
¶
Index ¶
- Constants
- Variables
- func ExtractVersionFromShorthand(shorthand string) string
- func FormatGitHubShorthand(org, repo, path, version string) string
- func InitNotificationDedup(cooldown time.Duration)
- func IsBranch(version string) bool
- func IsEmpty(i any) bool
- func IsGitHubShorthand(s string) bool
- func IsSemanticVersion(version string) bool
- func IsVersioned(shorthand string) bool
- func NewConfigurableTransport(config *WebhookSecurityConfig) *http.Transport
- func NewMail(c *MailConfig) core.Middleware
- func NewOverlap(c *OverlapConfig) core.Middleware
- func NewSafeTransport() *http.Transport
- func NewSave(c *SaveConfig) core.Middleware
- func NewSlack(c *SlackConfig) core.Middlewaredeprecated
- func NewWebhook(config *WebhookConfig, loader *PresetLoader) (core.Middleware, error)
- func NewWebhookMiddleware(webhooks []core.Middleware) core.Middleware
- func ParseGitHubShorthand(shorthand string) (string, error)
- func ParseWebhookNames(s string) []string
- func RestoreHistory(saveFolder string, maxAge time.Duration, jobs []core.Job, logger *slog.Logger) error
- func SanitizeFilename(filename string) string
- func SanitizeJobName(jobName string) string
- func SanitizePath(path string) string
- func SetGlobalSecurityConfig(config *WebhookSecurityConfig)
- func SetTransportFactoryForTest(fn func() *http.Transport)
- func SetValidateWebhookURLForTest(fn func(string) error)
- func StripVersionFromShorthand(shorthand string) string
- func ValidateGitHubShorthand(shorthand string) error
- func ValidateWebhookURLImpl(rawURL string) error
- type CacheStats
- type GitHubShorthand
- type Mail
- type MailConfig
- type NotificationDedup
- type Overlap
- type OverlapConfig
- type PathSanitizer
- type Preset
- type PresetCache
- type PresetDataForTemplate
- type PresetLoader
- type PresetVariable
- type SMTPTLSPolicy
- type Save
- type SaveConfig
- type Slack
- type SlackConfig
- type TriggerType
- type Webhook
- type WebhookConfig
- type WebhookData
- type WebhookExecutionData
- type WebhookGlobalConfig
- type WebhookHostData
- type WebhookJobData
- type WebhookManager
- func (m *WebhookManager) Get(name string) (*WebhookConfig, bool)
- func (m *WebhookManager) GetGlobalMiddlewares() ([]core.Middleware, error)
- func (m *WebhookManager) GetMiddlewares(names []string) ([]core.Middleware, error)
- func (m *WebhookManager) GlobalWebhookNames() []string
- func (m *WebhookManager) Register(config *WebhookConfig) error
- type WebhookMiddleware
- type WebhookOfeliaData
- type WebhookSecurityConfig
- type WebhookSecurityValidator
Constants ¶
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 ¶
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
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
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
var ( ErrDangerousPattern = errors.New("invalid path: contains dangerous pattern") ErrSystemDirectory = errors.New("invalid path: cannot write to system directory") )
Sanitize errors
var DefaultSanitizer = NewPathSanitizer()
Default sanitizer instance
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.
var TransportFactory = func() *http.Transport {
fn := getTransportFactory()
return fn()
}
TransportFactory creates HTTP transports for webhook requests (thread-safe access)
var ValidateWebhookURL = func(rawURL string) error {
fn := getValidateWebhookURL()
return fn(rawURL)
}
ValidateWebhookURL validates a URL for webhook requests (thread-safe access)
var Version = "dev"
Version is set during build and used in webhook templates
Functions ¶
func ExtractVersionFromShorthand ¶ added in v0.16.0
ExtractVersionFromShorthand extracts the version from a shorthand
func FormatGitHubShorthand ¶ added in v0.16.0
FormatGitHubShorthand creates a shorthand string from components
func InitNotificationDedup ¶ added in v0.16.0
InitNotificationDedup initializes the global deduplicator with the specified cooldown period. Call this during configuration loading.
func IsGitHubShorthand ¶ added in v0.16.0
IsGitHubShorthand checks if a string is a GitHub shorthand
func IsSemanticVersion ¶ added in v0.16.0
IsSemanticVersion checks if a version string looks like a semantic version
func IsVersioned ¶ added in v0.16.0
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
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
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
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
SanitizeFilename is a convenience function using the default sanitizer
func SanitizeJobName ¶ added in v0.10.1
SanitizeJobName is a convenience function using the default sanitizer
func SanitizePath ¶ added in v0.10.1
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
SetTransportFactoryForTest allows tests to override the transport factory (thread-safe)
func SetValidateWebhookURLForTest ¶ added in v0.17.0
SetValidateWebhookURLForTest allows tests to override the URL validator (thread-safe)
func StripVersionFromShorthand ¶ added in v0.16.0
StripVersionFromShorthand removes the version from a shorthand
func ValidateGitHubShorthand ¶ added in v0.16.0
ValidateGitHubShorthand validates that a GitHub shorthand is well-formed
func ValidateWebhookURLImpl ¶ added in v0.16.0
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
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 ¶
ContinueOnStop always returns true; we always want to report the final status
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 ¶
ContinueOnStop Overlap is only called if the process is still 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
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
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.)
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 ¶
ContinueOnStop always returns true; we always want to report the final status
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 ¶
ContinueOnStop always returns true; we always want to report the final status
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
ContinueOnStop returns true because we want to report final execution status
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
WebhookHostData contains host information for templates
type WebhookJobData ¶ added in v0.16.0
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.