Documentation
¶
Overview ¶
Package gorun runs plain shell commands, defined in a Gorunfile, on one or more servers over SSH or on the local machine, in parallel.
The gorun command (in ./cmd) is a thin layer over this package: it adds the picker, the live view and the history. Use the package directly to run Gorunfiles from your own code.
To learn how to write a Gorunfile, start with the tutorial: https://github.com/jenmud/gorun/blob/master/docs/tutorial.md
Gorunfiles ¶
A Gorunfile is TOML. It has a library of named tasks (Task): a shell command, script or template, with optional file copies, sudo, env vars, dependencies, a timeout and retries. Servers (Server) pick tasks from the library by name, directly or through named task lists (Bundle) and shared settings (ServerGroup). A server with SSH settings runs its tasks over SSH; one without them runs on the local machine. A task runs on one kind of server only: a task with local = true (Task.Local) runs only on local servers, and every other task runs only over SSH. Tasks of the other kind, and tasks that depend on them, are left out of a server's pipeline.
title = "Provision app servers"
[tasks.hello]
cmd_template = "echo hello from {{.Name}}"
[servers]
tasks = ["hello"]
[servers.hosts.web-01]
hostname = "web-01.example.com"
[servers.hosts.web-01.ssh]
username = "deploy"
Every option is described in https://github.com/jenmud/gorun/blob/master/docs/gorunfile.md.
Loading and running ¶
Config.Load reads and validates a Gorunfile, and Config.ResolvePaths makes its relative paths relative to the file. Config.Run then runs every server's tasks, or just the servers you pass it:
f, err := os.Open("Gorunfile")
if err != nil {
return err
}
defer f.Close()
var cfg gorun.Config
if err := cfg.Load(f); err != nil {
return err
}
cfg.ResolvePaths(filepath.Dir(f.Name()))
return cfg.Run(ctx)
Choosing servers and tasks ¶
Config.SelectServers picks servers the way the --server, --tag and --group flags do, and Config.ExpandTaskPatterns expands task names, globs and "@bundle" references the way --task does:
servers, err := cfg.SelectServers(gorun.ServerFilter{Tags: []string{"web"}})
if err != nil {
return err
}
tasks, err := cfg.ExpandTaskPatterns("@baseline")
if err != nil {
return err
}
for i := range servers {
servers[i].TaskNames = tasks
}
return cfg.Run(ctx, servers...)
Config.ServersPipeline returns every server with its task list resolved, as gorun --list-servers shows it.
Running single tasks and running locally ¶
Config.RunTask runs one task (with its dependencies) on some or all servers, whether or not they normally run it, and returns an error when none of them can run it (see Task.Local). Config.RunLocal runs tasks on this machine without any SSH server; only tasks marked local = true run there. LocalServer and Config.LocalServer give a Server that runs locally.
Events ¶
Set Config.OnEvent to follow a run as it happens. It receives a ProcessStartedEvent, a PlanEvent per server, a TaskEvent for every change in a task's TaskState, a ServerErrorEvent when a server can't run, and a ProcessFinishedEvent at the end:
cfg.OnEvent = func(event any) {
if ev, ok := event.(gorun.TaskEvent); ok && ev.State != gorun.TaskRunning {
fmt.Printf("%s: %s exit %d\n", ev.Server, ev.Task, ev.ExitCode)
}
}
Index ¶
- Constants
- Variables
- func CopyFiles(client *ssh.Client, server any, task *Task) error
- func EnvLine(vars map[string]string) (string, error)
- func ExpandEnv(value string) string
- func LogFilePath() string
- func LogOutput() io.Writer
- func NewLogger(level string, w io.Writer, json bool) *slog.Logger
- func NewSSHClient(ctx context.Context, server Server) (*ssh.Client, error)
- func OpenLogFile(path string) (*os.File, error)
- func SetLogOutput(w io.Writer)
- func StartNATS()
- type Bundle
- type Config
- func (c *Config) AllServers() []Server
- func (c *Config) ExpandTaskPatterns(patterns ...string) ([]string, error)
- func (c *Config) ExpandTaskRefs(refs ...string) ([]string, error)
- func (c *Config) Load(r io.Reader) error
- func (c *Config) LocalServer() Server
- func (c *Config) ResolvePaths(dir string)
- func (c *Config) Run(ctx context.Context, server ...Server) error
- func (c *Config) RunLocal(ctx context.Context, names ...string) error
- func (c *Config) RunTask(ctx context.Context, t Task, server ...Server) error
- func (c *Config) SelectServers(f ServerFilter) ([]Server, error)
- func (c *Config) ServerPipeline(server Server) (Server, error)
- func (c *Config) ServersPipeline() (map[string]Server, error)
- func (c *Config) TaskNames() []string
- type Copy
- type PlanEvent
- type ProcessFinishedEvent
- type ProcessStartedEvent
- type RunDoneEvent
- type SSH
- type Server
- type ServerErrorEvent
- type ServerFilter
- type ServerGroup
- type Task
- type TaskEvent
- type TaskExec
- type TaskMeta
- type TaskState
Examples ¶
Constants ¶
const ( // DefaultTaskTimeout is how long a task may run when it doesn't set its own // timeout. It stops a hung command from holding up a run forever. DefaultTaskTimeout = 30 * time.Minute // DefaultRetryDelay is how long gorun waits between attempts when a task // sets retries but not retry_delay. A short pause gives transient failures // (a restarting service, a busy package mirror) a moment to settle. DefaultRetryDelay = 50 * time.Millisecond // RerunSafe and RerunUnsafe are the accepted values of Task.Rerun. An // empty value means the author has not said whether rerunning is safe. RerunSafe = "safe" RerunUnsafe = "unsafe" )
const BundlePrefix = "@"
BundlePrefix marks a task list entry as a reference to a bundle rather than a task, e.g. "@base" for [bundles.base].
Variables ¶
var ErrNoTasks = errors.New("no tasks to run: local servers only run tasks marked local = true, and other tasks only run over SSH")
ErrNoTasks is returned by Run when no selected server has a task to run. Local servers only run tasks marked local = true, and other tasks only run over SSH.
var LocalServer = Server{Name: "local"}
LocalServer is a ready-to-use Server that runs tasks on the local machine.
Functions ¶
func CopyFiles ¶
CopyFiles copies files for a task to a server. Templates are executed with server as their data, the same as cmd_template.
func EnvLine ¶ added in v0.8.0
EnvLine renders vars as a single `export KEY='value' ...` shell line, keys upper-cased and sorted. It is the exported form of envLine.
func LogFilePath ¶
func LogFilePath() string
LogFilePath returns the file gorun writes its logs to: GORUN_LOG_FILE when set (a leading ~ is expanded), otherwise ~/.gorun/gorun.log.
func LogOutput ¶
LogOutput returns the current destination for task and server logs, so it can be put back after something interactive silences it.
func NewSSHClient ¶
NewSSHClient creates a new SSH client connected to the provided server.
func OpenLogFile ¶
OpenLogFile opens path for appending, creating it and its directory if needed.
func SetLogOutput ¶
SetLogOutput overrides the destination for task and server logs, so they go to the log file instead of cluttering the terminal.
Types ¶
type Bundle ¶
type Bundle struct {
Name string `toml:"-"` // the <name> in [bundles.<name>]
Description string `toml:"description"`
Tasks []string `toml:"tasks"`
Vars map[string]any `toml:"vars,omitempty"`
}
Bundle is a named list of tasks, defined as [bundles.<name>]. Task lists refer to it as "@<name>", and it expands to its tasks in order. A bundle may include other bundles.
type Config ¶
type Config struct {
Title string `toml:"title"`
GorunUUID string `toml:"-"`
LogLevel string `toml:"log_level"`
MaxParallel int `toml:"max_parallel"` // 0 means run every selected host at once
MaxTaskParallel int `toml:"max_task_parallel"` // 0 (default) = one at a time; -1 = all at once
EnvVars map[string]string `toml:"envvars"`
Vars map[string]any `toml:"vars,omitempty"`
SSH SSH `toml:"ssh"`
// Tasks is the task library, keyed by task name. Servers pick tasks from it by name.
Tasks map[string]Task `toml:"tasks"`
// Bundles are named task lists, referred to as "@<name>" wherever a task list is accepted.
Bundles map[string]Bundle `toml:"bundles"`
// Groups hold defaults shared by the servers that join them.
Groups map[string]ServerGroup `toml:"server_groups"`
// Servers are the [servers.hosts.<name>] tables, in the order they appear in the file.
Servers []Server `toml:"-"`
// OnEvent, when set, receives the events in events.go as tasks run.
OnEvent func(event any) `toml:"-"`
// SelectedTasks, when non-empty, limits execution to these tasks (and their
// dependencies). Bundle references ("@name") are expanded. Used by the --task CLI flag.
SelectedTasks []string `toml:"-"`
}
Config represents a config file.
func (*Config) AllServers ¶ added in v0.5.0
AllServers returns a copy of every server in the config, in config order. Use it to browse or filter the full inventory (e.g. for a picker or a "run on local" toggle).
func (*Config) ExpandTaskPatterns ¶
ExpandTaskPatterns turns --task style arguments into task names. Each argument is a task name, a glob such as "deploy-*", or a bundle reference ("@base", or a glob like "@web-*"). A glob must match at least one task or bundle.
func (*Config) ExpandTaskRefs ¶
ExpandTaskRefs turns a task list into task names: "@<bundle>" entries are replaced by the bundle's tasks, and repeated names are dropped, keeping the first. Every name must be a task in the library.
func (*Config) LocalServer ¶ added in v0.5.0
LocalServer returns a server that runs on this machine. If the config already defines a server with no SSH configured, that server is returned; otherwise a new "local" server is returned.
func (*Config) ResolvePaths ¶
ResolvePaths makes the relative file paths in the config (SSH identity files and task cmd_files) relative to dir, normally the directory holding the config file, so gorun can be run from anywhere.
func (*Config) RunLocal ¶ added in v0.5.0
RunLocal runs the named tasks (or every task when none are given) on the local machine. Only tasks marked local = true run there, and ErrNoTasks is returned when none of them are. Task names may be globs or bundle references ("@name"), just like --task.
Example ¶
Load a Gorunfile and run its tasks on this machine, printing each task as it finishes.
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/jenmud/gorun"
)
func main() {
gorunfile := `
title = "Example"
[tasks.hello]
cmd = "echo hello"
local = true
[tasks.goodbye]
cmd = "echo goodbye"
local = true
depends_on = ["hello"]
`
var cfg gorun.Config
if err := cfg.Load(strings.NewReader(gorunfile)); err != nil {
log.Fatal(err)
}
cfg.OnEvent = func(event any) {
if ev, ok := event.(gorun.TaskEvent); ok && ev.State == gorun.TaskDone {
fmt.Printf("%s: %s", ev.Task, ev.Stdout)
}
}
if err := cfg.RunLocal(context.Background(), "goodbye"); err != nil {
log.Fatal(err)
}
}
Output: hello: hello goodbye: goodbye
func (*Config) RunTask ¶
RunTask runs a single task (and its dependencies) on one or more servers. When no servers are given, every server in the config is used. The task does not need to be part of any server's configured task list.
func (*Config) SelectServers ¶
func (c *Config) SelectServers(f ServerFilter) ([]Server, error)
SelectServers returns the servers matching the filter, in config order. Exact names, tags and groups that don't exist are errors (with a suggestion for likely typos), as is a name glob that matches nothing or a filter that leaves no servers.
func (*Config) ServerPipeline ¶
ServerPipeline returns the server with its tasks, and their dependencies, resolved into run order.
func (*Config) ServersPipeline ¶
ServersPipeline returns every server with its tasks resolved into run order.
type Copy ¶
type Copy struct {
Src string `toml:"src"`
SrcFile string `toml:"src_file,omitempty"`
SrcTpl string `toml:"src_template,omitempty"`
SrcTmpl string `toml:"src_tmpl,omitempty"`
Template string `toml:"template,omitempty"`
Content string `toml:"content,omitempty"`
Dst string `toml:"dst"`
DstPath string `toml:"dst_path,omitempty"`
Dir string `toml:"dir,omitempty"`
Dest string `toml:"dest,omitempty"`
Mode string `toml:"mode,omitempty"`
Perm string `toml:"perm,omitempty"`
}
Copy represents a file copy operation.
type PlanEvent ¶
type PlanEvent struct {
Type string `json:"type"`
GorunUUID string `json:"gorun_uuid"`
Server string `json:"server"`
Address string `json:"address"`
Tasks []string `json:"tasks"`
TaskMeta []TaskMeta `json:"task_meta,omitempty"`
EnvVars map[string]string `json:"envvars,omitempty"`
Vars map[string]any `json:"vars,omitempty"`
}
PlanEvent is sent once per server, before any task runs, with the tasks in execution order.
type ProcessFinishedEvent ¶
type ProcessFinishedEvent struct {
Type string `json:"type"`
GorunUUID string `json:"gorun_uuid"`
Success bool `json:"success"`
Duration time.Duration `json:"duration"`
TotalTasks int `json:"total_tasks"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
FinishedAt time.Time `json:"finished_at"`
}
ProcessFinishedEvent is published when a gorun process finishes running.
type ProcessStartedEvent ¶
type ProcessStartedEvent struct {
Type string `json:"type"`
GorunUUID string `json:"gorun_uuid"`
Title string `json:"title"`
Servers []string `json:"servers"`
StartedAt time.Time `json:"started_at"`
Hostname string `json:"hostname"`
PID int `json:"pid"`
EnvVars map[string]string `json:"envvars,omitempty"`
Vars map[string]any `json:"vars,omitempty"`
}
ProcessStartedEvent is published when a gorun process starts running.
type RunDoneEvent ¶
type RunDoneEvent struct {
Type string `json:"type"`
GorunUUID string `json:"gorun_uuid"`
Err error `json:"error,omitempty"`
}
RunDoneEvent is sent when every server has finished.
type SSH ¶
type SSH struct {
User string `toml:"username"`
Password string `toml:"password"`
Port int `toml:"port"`
Identityfile string `toml:"identityfile"`
ConfigFile string `toml:"config_file,omitempty"`
Config string `toml:"config,omitempty"`
}
SSH represents SSH creds
type Server ¶
type Server struct {
LogLevel string `toml:"log_level"`
MaxTaskParallel int `toml:"max_task_parallel"` // 0 (default) = one at a time; -1 = all at once
Name string `toml:"-"` // the <name> in [servers.hosts.<name>]
Hostname string `toml:"hostname"`
SSH SSH `toml:"ssh"`
// TaskNames are the tasks to run, by name. Load replaces them with the server's
// final task list: its own tasks (or the global and group lists), plus AddTasks,
// minus SkipTasks, with bundles expanded.
TaskNames []string `toml:"tasks"`
// Groups are the [server_groups.<name>] this server joins, in order. Load applies
// their defaults to the server.
Groups []string `toml:"groups"`
// Tags label the server for --tag selection. Load adds its groups' tags.
Tags []string `toml:"tags"`
// EnvVars are exported to every task on this server. Load merges in its groups'
// envvars; a task's own envvars still win.
EnvVars map[string]string `toml:"envvars"`
// Vars are template variables for this server (and its tasks). Load merges in its
// groups' vars; a task's own vars still win in its templates.
Vars map[string]any `toml:"vars,omitempty"`
// AddTasks are appended to the server's task list and SkipTasks removed from it.
AddTasks []string `toml:"add_tasks"`
SkipTasks []string `toml:"skip_tasks"`
// Tasks is the resolved pipeline, including dependencies, set by ServerPipeline.
Tasks []Task `toml:"-"`
}
Server is a server to connect to and execute tasks.
type ServerErrorEvent ¶
type ServerErrorEvent struct {
Type string `json:"type"`
GorunUUID string `json:"gorun_uuid"`
Server string `json:"server"`
Err error `json:"error"`
}
ServerErrorEvent is sent when a server can't run its tasks at all (e.g. SSH connection failed).
type ServerFilter ¶
ServerFilter selects servers. Every set field must match:
- Names: the server name matches any of these names or globs.
- Tags: the server has every one of these tags.
- Groups: the server is in every one of these groups.
- Exclude: the server name matches none of these names or globs.
func (ServerFilter) IsZero ¶
func (f ServerFilter) IsZero() bool
IsZero reports whether the filter selects every server.
type ServerGroup ¶
type ServerGroup struct {
Name string `toml:"-"` // the <name> in [server_groups.<name>]
LogLevel string `toml:"log_level"`
Tags []string `toml:"tags"`
Tasks []string `toml:"tasks"`
EnvVars map[string]string `toml:"envvars"`
Vars map[string]any `toml:"vars,omitempty"`
SSH SSH `toml:"ssh"`
}
ServerGroup holds defaults shared by the servers that join it, defined as [server_groups.<name>]. A server joins groups by listing them in its groups key.
type Task ¶
type Task struct {
LogLevel string `toml:"log_level"`
Name string `toml:"-"` // the <name> in [tasks.<name>]
Sudo bool `toml:"sudo"`
Cmd string `toml:"cmd,omitempty"`
CmdFile string `toml:"cmd_file,omitempty"`
CmdTemplate string `toml:"cmd_template,omitempty"`
CmdTemplateFile string `toml:"cmd_template_file,omitempty"`
Copy []Copy `toml:"copy,omitempty"`
Copies []Copy `toml:"copies,omitempty"`
Upload []Copy `toml:"upload,omitempty"`
Files []Copy `toml:"files,omitempty"`
EnvVars map[string]string `toml:"envvars"`
Vars map[string]any `toml:"vars,omitempty"`
DependsOn []string `toml:"depends_on"`
Shell string `toml:"shell"`
Timeout time.Duration `toml:"timeout,omitempty"`
// Description is a one-line summary shown in task lists and the TUI.
Description string `toml:"description,omitempty"`
// Details is an optional longer explanation shown when a task is expanded.
Details string `toml:"details,omitempty"`
// Rerun is RerunSafe or RerunUnsafe. Empty means unspecified.
Rerun string `toml:"rerun,omitempty"`
// Local, when true, runs the task only on local servers and skips it on any
// server that connects over SSH. Tasks without it run only over SSH.
Local bool `toml:"local,omitempty"`
// Retries is how many extra times to run a failed task. Zero means run once.
Retries int `toml:"retries,omitempty"`
// RetryDelay is how long to wait before each retry. Unset uses
// DefaultRetryDelay; an explicit zero retries immediately.
RetryDelay *time.Duration `toml:"retry_delay,omitempty"`
// RetryUnsafe allows automatic retries for a task marked rerun = "unsafe".
RetryUnsafe bool `toml:"retry_unsafe,omitempty"`
}
Task is a task to execute on one or more servers.
func (Task) CanRetry ¶
CanRetry reports whether a failed attempt may be retried. Tasks marked unsafe to rerun require an explicit RetryUnsafe opt-in.
func (*Task) CommandFor ¶
CommandFor reads the command from either inline cmd or from a file, and compiles templates with the provided server.
func (Task) RetryDelayDuration ¶
RetryDelayDuration returns the wait between attempts. A task that doesn't set retry_delay uses DefaultRetryDelay.
func (Task) TimeoutDuration ¶
TimeoutDuration returns the timeout to apply to the task. A zero (or negative) Timeout uses DefaultTaskTimeout.
type TaskEvent ¶
type TaskEvent struct {
Type string `json:"type"`
GorunUUID string `json:"gorun_uuid"`
Server string `json:"server"`
Task string `json:"task"`
State TaskState `json:"state"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
Command string `json:"command,omitempty"`
EnvVars map[string]string `json:"envvars,omitempty"`
Vars map[string]any `json:"vars,omitempty"`
ExitCode int `json:"exit_code"`
Err error `json:"error,omitempty"`
Duration time.Duration `json:"duration"`
// Attempt and Attempts are set when a task has retries configured. A
// running event with Attempt greater than one means the previous attempt
// failed and the task is being retried.
Attempt int `json:"attempt,omitempty"`
Attempts int `json:"attempts,omitempty"`
}
TaskEvent is sent when a task changes state.
type TaskExec ¶
type TaskExec struct {
// contains filtered or unexported fields
}
TaskExec is a execution task to be run on a server.
func NewLocalTaskExec ¶ added in v0.5.0
NewLocalTaskExec creates TaskExecs that run on the local machine (no SSH).
func NewTaskExec ¶
NewTaskExec creates and sets up a task to be executed on the remote session.
func (*TaskExec) LocalExecute ¶
LocalExecute executes the task as a local task.
type TaskMeta ¶
type TaskMeta struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Details string `json:"details,omitempty"`
Rerun string `json:"rerun,omitempty"`
}
TaskMeta carries the documentation attached to a task in a PlanEvent, so consumers such as the TUI and web dashboard can show descriptions and rerun warnings. Tasks still lists the bare names for older consumers.
