hooks

package
v0.0.0-...-99cc010 Latest Latest
Warning

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

Go to latest
Published: May 6, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package hooks lets users attach commands to the lifecycle events of a Locorum site (start, stop, delete, clone, version-change, multisite, export). Each hook is one of three task types:

  • exec: runs in a per-site Docker container (default: php).
  • exec-host: runs in a host shell (bash on Unix/WSL, cmd on Windows).
  • wp-cli: convenience wrapper over `wp …` in the php container.

The runner is GUI-agnostic: it streams output through callbacks and writes a complete on-disk log per Run. The SiteManager is responsible for firing pre/post hooks at every lifecycle method; the UI is responsible for rendering output and presenting the editor.

Concurrency: hooks within a single event run sequentially in position order. Different events on different sites may run concurrently; the SiteManager owns a per-site mutex so two events on the same site do not interleave.

Index

Constants

View Source
const (
	// DefaultMaxLogsPerSite — applied per (slug, event) family.
	DefaultMaxLogsPerSite = 50
	// DefaultLogMaxAge — applied across the entire LogsBaseDir.
	DefaultLogMaxAge = 30 * 24 * time.Hour
)
View Source
const (
	SettingKeyFailGlobal = "hooks.fail_on_error.global"
	SettingKeyFailPrefix = "hooks.fail_on_error."
)

SettingKeyFailGlobal is the storage key for the global "fail on error" toggle. Per-site keys are SettingKeyFailPrefix+<siteID>.

View Source
const DefaultTaskTimeout = 5 * time.Minute

DefaultTaskTimeout is the per-task wall-clock deadline. Hooks should not run for longer than this; the runner cancels the task's context when it elapses. v1 uses a global default; the schema reserves room for a per-hook column.

View Source
const DefaultsPath = "config/hooks/defaults.json"

DefaultsPath is the embedded path used by Locorum at startup.

View Source
const HostDBPort = "3306"

HostDBPort is the host port published by the site database container in host-context tasks. Locorum currently does not publish per-site DB ports to the host; users typically connect via Adminer at db.localhost. We expose the in-container port (3306) here as a sensible default. If/when the platform starts publishing per-site DB ports, swap this for a lookup against the docker port mapping.

Variables

View Source
var (
	// ErrHookInvalid is the umbrella sentinel for every Validate failure.
	ErrHookInvalid = errors.New("hook is invalid")

	// ErrEmptyCommand is returned by Validate when Command is blank.
	ErrEmptyCommand = errors.New("hook command is empty")

	// ErrSkipped is returned by the runner when LOCORUM_SKIP_HOOKS is set.
	// Run() returns nil but the Summary is empty. Useful for tests to detect
	// the skip behaviour explicitly.
	ErrSkipped = errors.New("hooks skipped: LOCORUM_SKIP_HOOKS=1")
)

Sentinel errors. Use errors.Is to test.

Functions

func BuildEnv

func BuildEnv(site *types.Site, ctx EnvContext) []string

BuildEnv returns the complete LOCORUM_* environment variable list to inject into a hook task. The result is a fresh slice; the caller is free to append to it.

Variables are sorted by name so the output is reproducible across calls (helpful for diffs in tests and reasoning about ordering precedence).

The returned slice is "KEY=VALUE" formatted, ready to pass to either docker.ExecOptions.Env or utils.HostExecOptions.Env.

func SweepLogs

func SweepLogs(baseDir string, maxAge time.Duration, maxPerSite int) error

SweepLogs prunes old run-log files. Safe to call from startup. Errors from individual files are logged at warn level and do not abort the sweep.

Types

type Config

type Config struct {
	Lister      HookLister
	Container   ContainerExecer
	Host        HostExecer
	Settings    SettingsReader
	LogsBaseDir string // typically ~/.locorum/hooks/runs/

	// SkipEnvVar is the environment variable name that, if set to "1",
	// short-circuits Run() with ErrSkipped. Defaults to LOCORUM_SKIP_HOOKS.
	SkipEnvVar string

	// MaxLogsPerSite caps the number of run-log files retained per site
	// after the startup sweep. Older files are removed.
	MaxLogsPerSite int
	// LogMaxAge caps the age of retained log files. Older files are removed.
	LogMaxAge time.Duration
}

Config wires the runner's external dependencies. Every field is required in production; tests inject fakes.

type ContainerExecOptions

type ContainerExecOptions struct {
	Cmd        []string
	Env        []string
	User       string
	WorkingDir string
}

ContainerExecOptions mirrors docker.ExecOptions but is decoupled so the hooks package does not need to import docker. The runner-side glue layer translates between the two.

type ContainerExecer

type ContainerExecer interface {
	ExecInContainerStream(ctx context.Context, containerName string, opts ContainerExecOptions, onLine func(string, bool)) (int, error)
}

ContainerExecer is the narrow port the runner uses to run an exec task inside a Docker container. The production implementation is docker.Docker.ExecInContainerStream; tests inject a fake.

type DockerContainerExecer

type DockerContainerExecer struct {
	D *docker.Docker
}

DockerContainerExecer wraps *docker.Docker to satisfy ContainerExecer.

The narrow ContainerExecer interface lets the runner stay decoupled from the docker package; this adapter is the only place where the two types meet. Move it here (rather than into internal/docker) so the docker package never imports hooks.

func (DockerContainerExecer) ExecInContainerStream

func (a DockerContainerExecer) ExecInContainerStream(ctx context.Context, container string, opts ContainerExecOptions, onLine func(string, bool)) (int, error)

ExecInContainerStream implements ContainerExecer.

type EnvContext

type EnvContext int

EnvContext selects which network perspective the env vars should describe. Container-context vars resolve database addresses to the in-network alias (database:3306); host-context vars resolve them to 127.0.0.1 plus the published port.

const (
	// ContextContainer is for tasks that run inside one of the site's
	// containers (exec, wp-cli).
	ContextContainer EnvContext = iota

	// ContextHost is for tasks that run on the host shell (exec-host).
	ContextHost
)

func (EnvContext) String

func (c EnvContext) String() string

String returns a stable label suitable for diagnostics.

type Event

type Event string

Event identifies a lifecycle moment at which hooks fire. Events are paired (pre-X / post-X) but each is independent; pre-X failure does not prevent post-X from running on a different invocation.

const (
	PreStart  Event = "pre-start"
	PostStart Event = "post-start"

	PreStop  Event = "pre-stop"
	PostStop Event = "post-stop"

	PreDelete  Event = "pre-delete"
	PostDelete Event = "post-delete"

	PreClone  Event = "pre-clone"
	PostClone Event = "post-clone"

	PreVersionsChange  Event = "pre-versions-change"
	PostVersionsChange Event = "post-versions-change"

	PreMultisite  Event = "pre-multisite"
	PostMultisite Event = "post-multisite"

	PreExport  Event = "pre-export"
	PostExport Event = "post-export"

	PreImportDB  Event = "pre-import-db"
	PostImportDB Event = "post-import-db"

	PreSnapshot  Event = "pre-snapshot"
	PostSnapshot Event = "post-snapshot"
)

Active events — fire today.

const (
	PreImportFiles  Event = "pre-import-files"
	PostImportFiles Event = "post-import-files"

	PreRestoreSnapshot  Event = "pre-restore-snapshot"
	PostRestoreSnapshot Event = "post-restore-snapshot"

	PreImportSite  Event = "pre-import-site"
	PostImportSite Event = "post-import-site"
)

Reserved events — declared but not yet fired by any lifecycle method. Adding the firing site is a one-line runner.Run(...) addition once the underlying feature lands.

func ActiveEvents

func ActiveEvents() []Event

ActiveEvents returns the events currently fired by the SiteManager.

func AllEvents

func AllEvents() []Event

AllEvents returns every recognised event, including reserved ones. The slice is a copy so callers may sort/filter freely.

func SortedActiveEvents

func SortedActiveEvents() []Event

SortedActiveEvents is ActiveEvents() sorted lexically — convenient when the UI wants a stable display order.

func (Event) AllowsContainerTasks

func (e Event) AllowsContainerTasks() bool

AllowsContainerTasks reports whether the event fires while the site's containers are running (and so exec / wp-cli tasks are sensible).

The "pre-X" events for lifecycle methods that bring containers up are rejected: the containers don't exist yet. Similarly, post-stop and pre/post-delete fire when containers are down or being torn down.

func (Event) Valid

func (e Event) Valid() bool

Valid reports whether e is a known event.

type Hook

type Hook struct {
	ID        int64    `json:"id"`
	SiteID    string   `json:"siteId"`
	Event     Event    `json:"event"`
	Position  int      `json:"position"`
	TaskType  TaskType `json:"taskType"`
	Command   string   `json:"command"`
	Service   string   `json:"service"`   // exec only: web|php|database|redis
	RunAsUser string   `json:"runAsUser"` // exec only: e.g. "root" or "1000:1000"
	Enabled   bool     `json:"enabled"`
	CreatedAt string   `json:"createdAt"`
	UpdatedAt string   `json:"updatedAt"`
}

Hook is a user-defined command attached to a lifecycle Event.

Hooks are persisted per-site in the site_hooks table; ID is the SQLite row id. Position controls execution order within an event (lower runs first; storage assigns the next free position on insert).

func (Hook) Validate

func (h Hook) Validate() error

Validate checks the hook's intrinsic and event-relative invariants. It is called both at save time (storage layer rejects invalid hooks) and at run time (defence-in-depth). All errors return ErrHookInvalid wrapped with a human-readable message so the GUI can show it directly.

type HookLister

type HookLister interface {
	ListHooksByEvent(siteID string, ev Event) ([]Hook, error)
}

HookLister loads the persisted hooks for a (site, event). The runner does not write to storage; that is the GUI's job.

type HostExecOptions

type HostExecOptions struct {
	Command string
	Cwd     string
	Env     []string
}

HostExecOptions mirrors utils.HostExecOptions for the same reason.

type HostExecer

type HostExecer interface {
	RunHostStream(ctx context.Context, opts HostExecOptions, onLine func(string, bool)) (int, error)
}

HostExecer is the narrow port the runner uses to run an exec-host task on the host shell. The production implementation is utils.RunHostStream wrapped to match this signature.

type Result

type Result struct {
	Hook       Hook
	StartedAt  time.Time
	FinishedAt time.Time
	ExitCode   int
	Err        error
	// Stderr is true if stderr lines were observed.
	StderrSeen bool
	// LinesEmitted is the total number of stdout+stderr lines streamed.
	LinesEmitted int
	// LogPath is the absolute path of the per-event log file the runner
	// writes during the Run.
	LogPath string
}

Result describes the outcome of a single task execution.

func (Result) Duration

func (r Result) Duration() time.Duration

Duration returns the elapsed run time of a result.

func (Result) Succeeded

func (r Result) Succeeded() bool

Succeeded reports whether the task ran to completion with exit code 0.

type RunOptions

type RunOptions struct {
	OnTaskStart func(Hook)
	OnOutput    func(line string, stderr bool)
	OnTaskDone  func(Result)
	OnAllDone   func(Summary)
}

RunOptions bundles the streaming callbacks. Every field is optional; nil callbacks are no-ops. Callbacks fire on the runner's goroutine — if a callback blocks, the runner blocks. Implementations should hand work off to channels or goroutines if they need to do more than copy a string.

type Runner

type Runner interface {
	// Run executes every enabled hook for ev attached to site, in position
	// order. Returns the first task error if fail-strict mode is on AND a
	// task fails; otherwise nil. Runner returns nil if no hooks are
	// configured or LOCORUM_SKIP_HOOKS is set.
	Run(ctx context.Context, ev Event, site *types.Site, opts RunOptions) error

	// RunOne executes a single in-memory hook (not necessarily persisted).
	// Used by the "Run now" GUI button and the editor's "Test" button.
	// Always returns the task's error verbatim; fail-strict semantics do
	// not apply to a single task.
	RunOne(ctx context.Context, h Hook, site *types.Site, opts RunOptions) (Result, error)
}

Runner executes hooks for a lifecycle Event. One Runner per process.

func NewRunner

func NewRunner(cfg Config) (Runner, error)

NewRunner constructs a runner from cfg. Required fields are checked; nil returns an explanatory error rather than panicking later.

type SettingsReader

type SettingsReader interface {
	GetSetting(key string) (string, error)
}

SettingsReader reads named user settings. Used to look up the per-site / global "fail on hook error" toggle.

type Summary

type Summary struct {
	Event     Event
	SiteID    string
	Total     int
	Succeeded int
	Failed    int
	Skipped   int
	Aborted   bool // true if fail-strict caused early termination
	Duration  time.Duration
	LogPath   string
}

Summary aggregates per-Run statistics. Emitted via RunOptions.OnAllDone.

type TaskType

type TaskType string

TaskType identifies how a Hook's command is dispatched.

const (
	// TaskExec runs the command inside one of the site's running containers
	// (default: php). Use Hook.Service to target web/database/redis instead.
	TaskExec TaskType = "exec"

	// TaskExecHost runs the command on the host shell. Cwd defaults to the
	// site's FilesDir.
	TaskExecHost TaskType = "exec-host"

	// TaskWPCLI runs `wp <command>` inside the php container. Equivalent to
	// TaskExec with `wp ` prepended; offered as a separate type so the GUI
	// can validate and document the wp-cli use case.
	TaskWPCLI TaskType = "wp-cli"
)

func AllTaskTypes

func AllTaskTypes() []TaskType

AllTaskTypes returns the canonical, ordered list of task types.

func (TaskType) Valid

func (t TaskType) Valid() bool

Valid reports whether t is a known task type.

type Template

type Template struct {
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Event       Event    `json:"event"`
	TaskType    TaskType `json:"taskType"`
	Command     string   `json:"command"`
	Service     string   `json:"service,omitempty"`
}

Template is a one-click hook preset surfaced in the UI's "Templates" menu. Templates are inserts (not replacements) — clicking one appends a new hook to the user's site at the appropriate event.

func LoadTemplates

func LoadTemplates(efs embed.FS, path string) ([]Template, error)

LoadTemplates reads and validates the embedded defaults.json from the supplied filesystem at path. Pass DefaultsPath for production code; tests use a stub path under testdata/.

Validation: each template's TaskType + Event combination must satisfy Hook.Validate. Invalid templates return an error so a packaging mistake is caught at startup, not surfaced as a confusing GUI error later.

type UtilsHostExecer

type UtilsHostExecer struct{}

UtilsHostExecer wraps utils.RunHostStream so it satisfies HostExecer.

func (UtilsHostExecer) RunHostStream

func (UtilsHostExecer) RunHostStream(ctx context.Context, opts HostExecOptions, onLine func(string, bool)) (int, error)

RunHostStream implements HostExecer.

Directories

Path Synopsis
Package fake provides in-memory hooks.Runner / ContainerExecer / HostExecer implementations for tests in other packages.
Package fake provides in-memory hooks.Runner / ContainerExecer / HostExecer implementations for tests in other packages.

Jump to

Keyboard shortcuts

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