updater

package
v0.6.5 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package updater implements Etherpad-Go's self-update subsystem: a tiered (off / notify / manual / auto / autonomous) updater that checks GitHub releases, and — for writable single-binary installs — downloads the matching release binary, verifies it, atomically replaces the running executable and exits with code 75 so a process supervisor restarts into the new version.

It is a Go-appropriate port of Etherpad-lite's src/node/updater subsystem. Because Etherpad-Go ships as a single binary (rather than a git checkout updated with pnpm), the apply mechanism is binary self-replacement instead of git/pnpm, but the surrounding state machine, policy gates, scheduler, maintenance windows, session draining, rollback and crash-loop guard mirror the upstream design.

Index

Constants

View Source
const ExitCodeRestart = 75

ExitCodeRestart is returned to the OS after a successful apply or a rollback so that the supervisor (systemd, Docker, Windows service, ...) restarts the process into the swapped-in binary.

View Source
const SchemaVersion = 1

SchemaVersion is the persisted update-state schema version.

Variables

This section is empty.

Functions

func IsValidTag

func IsValidTag(tag string) bool

IsValidTag validates a release tag before it is interpolated into a URL or a filesystem path. It rejects empty/oversized tags, leading "-"/"." (option injection / hidden files), ".." (traversal), and control or shell-significant characters.

func SaveState

func SaveState(path string, s UpdateState) error

SaveState writes the state atomically (write to a temp file, then rename) so a crash mid-write can never leave a truncated state file.

Types

type CheckKind

type CheckKind string

CheckKind is the discriminator for a CheckResult.

const (
	CheckUpdated     CheckKind = "updated"
	CheckNotModified CheckKind = "notmodified"
	CheckPrerelease  CheckKind = "skipped-prerelease"
	CheckRateLimited CheckKind = "ratelimited"
	CheckError       CheckKind = "error"
)

type CheckResult

type CheckResult struct {
	Kind    CheckKind
	Release *ReleaseInfo
	ETag    string
	Status  int
}

CheckResult is the outcome of polling for the latest release.

type Config

type Config struct {
	Tier               Tier
	Repo               string
	CurrentVersion     string
	CheckInterval      time.Duration
	GraceMinutes       int
	DrainSeconds       int
	HealthCheckSeconds int
	InstallMethod      InstallMethod // override; auto-detected when InstallAuto
	Window             *MaintenanceWindow
	WindowConfigured   bool
	Verifier           SignatureVerifier
	StatePath          string
	LockPath           string
}

Config is the fully-resolved updater configuration (built from settings by the server package, keeping this package settings-free and testable).

type DrainResult

type DrainResult string

DrainResult is the outcome of a drain.

const (
	DrainCompleted DrainResult = "completed"
	DrainCancelled DrainResult = "cancelled"
)

type Drainer

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

Drainer blocks new pad connections for a countdown window and announces the countdown to connected clients, giving them time to save before the binary is swapped and the process restarts.

func NewDrainer

func NewDrainer(drainSecs int, setAccept func(bool), broadcast func(int)) *Drainer

NewDrainer constructs a drainer. setAccept must not be nil; broadcast may be.

func (*Drainer) Cancel

func (d *Drainer) Cancel()

Cancel aborts an in-progress drain.

func (*Drainer) Start

func (d *Drainer) Start() DrainResult

Start blocks until the drain window elapses or Cancel is called. New connections are refused for the duration and restored on return.

type ExecResult

type ExecResult struct {
	OK         bool
	BackupPath string
	Reason     string
}

ExecResult is the outcome of an apply attempt's file operations. The orchestrator turns OK into a pending-verification state + exit(75), and a failure into a rollback.

type Execution

type Execution struct {
	Status       ExecutionStatus `json:"status"`
	TargetTag    string          `json:"targetTag,omitempty"`
	FromVersion  string          `json:"fromVersion,omitempty"`
	BackupPath   string          `json:"backupPath,omitempty"`   // path to the backed-up previous binary
	ScheduledFor string          `json:"scheduledFor,omitempty"` // RFC3339
	DrainEndsAt  string          `json:"drainEndsAt,omitempty"`  // RFC3339
	DeadlineAt   string          `json:"deadlineAt,omitempty"`   // RFC3339, health-check deadline
	Reason       string          `json:"reason,omitempty"`
	At           string          `json:"at,omitempty"` // RFC3339, time the status was entered
}

Execution captures the current apply attempt. Fields are only meaningful for certain Status values.

type ExecutionStatus

type ExecutionStatus string

ExecutionStatus is the persisted state-machine position of an apply attempt.

const (
	StatusIdle                ExecutionStatus = "idle"
	StatusScheduled           ExecutionStatus = "scheduled"
	StatusPreflight           ExecutionStatus = "preflight"
	StatusPreflightFailed     ExecutionStatus = "preflight-failed"
	StatusDraining            ExecutionStatus = "draining"
	StatusExecuting           ExecutionStatus = "executing"
	StatusPendingVerification ExecutionStatus = "pending-verification"
	StatusVerified            ExecutionStatus = "verified"
	StatusRollingBack         ExecutionStatus = "rolling-back"
	StatusRolledBack          ExecutionStatus = "rolled-back"
	StatusRollbackFailed      ExecutionStatus = "rollback-failed" // terminal: blocks auto until acknowledged
)

type Executor

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

Executor downloads, verifies and atomically swaps in the new release binary.

func NewExecutor

func NewExecutor(checker *VersionChecker, exePath string, verifier SignatureVerifier, logger *zap.SugaredLogger) *Executor

NewExecutor builds an executor targeting exePath (the running executable).

func (*Executor) Apply

func (e *Executor) Apply(target ReleaseInfo) ExecResult

Apply performs the full download → verify → replace sequence. On success the previous binary is preserved at the returned BackupPath for rollback.

func (*Executor) Rollback

func (e *Executor) Rollback(backupPath string) error

Rollback restores the previous binary saved during Apply.

type InstallMethod

type InstallMethod string

InstallMethod describes how Etherpad-Go was installed, which determines whether an in-place update is possible.

const (
	InstallAuto    InstallMethod = "auto"    // detect at runtime
	InstallBinary  InstallMethod = "binary"  // writable standalone executable
	InstallDocker  InstallMethod = "docker"  // container; rollout handled by orchestrator
	InstallManaged InstallMethod = "managed" // read-only / package-managed
)

func DetectInstallMethod

func DetectInstallMethod(override InstallMethod) InstallMethod

DetectInstallMethod determines whether an in-place update is possible. An explicit, non-auto override is returned verbatim.

Detection order: a Docker environment is never self-updated (the orchestrator owns the image), then a standalone executable whose directory is writable is updatable as a binary, otherwise the install is treated as read-only/managed.

type LastResult

type LastResult struct {
	TargetTag   string `json:"targetTag"`
	FromVersion string `json:"fromVersion"`
	Outcome     string `json:"outcome"`
	Reason      string `json:"reason,omitempty"`
	At          string `json:"at"`
}

LastResult records the outcome of the most recent completed apply attempt for display in the admin UI.

type MaintenanceWindow

type MaintenanceWindow struct {
	StartMin int    // minutes since midnight, inclusive
	EndMin   int    // minutes since midnight, exclusive
	TZ       string // "utc" or "local"
}

MaintenanceWindow is a daily wall-clock window during which autonomous updates may run.

func ParseWindow

func ParseWindow(start, end, tz string) (*MaintenanceWindow, bool)

ParseWindow parses "HH:MM" start/end strings and a timezone ("utc"|"local"). It returns ok=false for malformed input or a zero-length window. When end <= start the window crosses midnight, e.g. 22:00–04:00.

func (*MaintenanceWindow) InWindow

func (w *MaintenanceWindow) InWindow(now time.Time) bool

InWindow reports whether now falls within the window.

func (*MaintenanceWindow) NextWindowStart

func (w *MaintenanceWindow) NextWindowStart(now time.Time) time.Time

NextWindowStart returns the earliest time at or after now whose wall-clock equals the window start.

type PolicyInput

type PolicyInput struct {
	Tier             Tier
	InstallMethod    InstallMethod
	CurrentVersion   string
	Latest           *ReleaseInfo
	ExecutionStatus  ExecutionStatus
	WindowConfigured bool // whether a maintenance window was configured at all
	WindowValid      bool // whether the configured window parsed successfully
}

PolicyInput is the pure input to EvaluatePolicy.

type PolicyResult

type PolicyResult struct {
	CanNotify     bool   // surface availability / send notifications
	CanManual     bool   // admin may trigger an apply
	CanAuto       bool   // scheduler may auto-apply (after a grace window)
	CanAutonomous bool   // scheduler may auto-apply respecting a maintenance window
	Reason        string // machine-readable explanation
}

PolicyResult is the set of permitted operations under the current config.

func EvaluatePolicy

func EvaluatePolicy(in PolicyInput) PolicyResult

EvaluatePolicy decides what the updater is permitted to do. It is pure and deterministic so it can be unit-tested without any I/O.

type PreflightDeps

type PreflightDeps struct {
	InstallMethod InstallMethod
	ExePath       string
	LockHeld      func() bool
}

PreflightDeps are the inputs to RunPreflight. All checks run before any file is touched, so a failed preflight is fully reversible.

type PreflightResult

type PreflightResult struct {
	OK     bool
	Reason string
}

PreflightResult reports whether an apply may proceed.

func RunPreflight

func RunPreflight(d PreflightDeps) PreflightResult

RunPreflight validates the environment before an apply. Cheap, reversible checks first.

type ReleaseInfo

type ReleaseInfo struct {
	Version     string `json:"version"` // tag without a leading "v"
	Tag         string `json:"tag"`
	Body        string `json:"body"`
	PublishedAt string `json:"publishedAt"`
	Prerelease  bool   `json:"prerelease"`
	HTMLURL     string `json:"htmlUrl"`
}

ReleaseInfo is the subset of a GitHub release the updater tracks.

type ScheduleAction

type ScheduleAction string

ScheduleAction is the outcome of DecideSchedule.

const (
	ScheduleNone   ScheduleAction = "none"
	ScheduleArm    ScheduleAction = "schedule"
	ScheduleCancel ScheduleAction = "cancel"
)

type ScheduleDecision

type ScheduleDecision struct {
	Action       ScheduleAction
	TargetTag    string
	ScheduledFor time.Time
}

ScheduleDecision tells the runner what to do with its timer.

func DecideSchedule

func DecideSchedule(in ScheduleInput) ScheduleDecision

DecideSchedule decides whether to arm, cancel or leave the auto-apply timer.

type ScheduleInput

type ScheduleInput struct {
	CanAuto       bool
	CanAutonomous bool
	Tier          Tier
	Latest        *ReleaseInfo
	Execution     Execution
	Now           time.Time
	GraceMinutes  int
	Window        *MaintenanceWindow
}

ScheduleInput is the pure input to DecideSchedule (called after each check).

type SignatureVerifier

type SignatureVerifier struct {
	Require   bool
	PublicKey string
}

SignatureVerifier optionally verifies the release checksums file against a trusted ed25519 public key (base64-encoded, 32 bytes). When Require is false verification is a no-op.

func (SignatureVerifier) Verify

func (s SignatureVerifier) Verify(message, signature []byte) error

Verify checks an ed25519 signature over message. It returns nil when signatures are not required.

type Status

type Status struct {
	Tier            Tier          `json:"tier"`
	InstallMethod   InstallMethod `json:"installMethod"`
	CurrentVersion  string        `json:"currentVersion"`
	Latest          *ReleaseInfo  `json:"latest,omitempty"`
	UpdateAvailable bool          `json:"updateAvailable"`
	Execution       Execution     `json:"execution"`
	LastResult      *LastResult   `json:"lastResult,omitempty"`
	Policy          PolicyResult  `json:"policy"`
}

Status is a snapshot for the admin UI.

type Tier

type Tier string

Tier controls how autonomous the updater is.

const (
	TierOff        Tier = "off"        // do nothing
	TierNotify     Tier = "notify"     // check + surface availability only
	TierManual     Tier = "manual"     // admin can trigger an apply
	TierAuto       Tier = "auto"       // scheduler auto-applies after a grace window
	TierAutonomous Tier = "autonomous" // scheduler auto-applies within a maintenance window
)

type TriggerAction

type TriggerAction string

TriggerAction is the outcome of DecideTriggerApply.

const (
	TriggerFire  TriggerAction = "fire"  // proceed with the apply
	TriggerDefer TriggerAction = "defer" // re-arm for NextAt (outside maintenance window)
	TriggerAbort TriggerAction = "abort" // state changed; do nothing
	TriggerClear TriggerAction = "clear" // policy revoked auto; clear the schedule
)

type TriggerDecision

type TriggerDecision struct {
	Action TriggerAction
	NextAt time.Time
}

TriggerDecision tells the runner what to do when its timer fires.

func DecideTriggerApply

func DecideTriggerApply(in TriggerInput) TriggerDecision

DecideTriggerApply decides whether a fired auto-apply timer should proceed.

type TriggerInput

type TriggerInput struct {
	Execution      Execution
	TimerTargetTag string // tag the timer was armed for
	Latest         *ReleaseInfo
	CanAuto        bool
	CanAutonomous  bool
	Tier           Tier
	Window         *MaintenanceWindow
	Now            time.Time
}

TriggerInput is the pure input to DecideTriggerApply (called on timer fire).

type UpdateState

type UpdateState struct {
	SchemaVersion int          `json:"schemaVersion"`
	LastCheckAt   string       `json:"lastCheckAt,omitempty"`
	LastETag      string       `json:"lastEtag,omitempty"`
	Latest        *ReleaseInfo `json:"latest,omitempty"`
	Execution     Execution    `json:"execution"`
	BootCount     int          `json:"bootCount"`
	LastResult    *LastResult  `json:"lastResult,omitempty"`
}

UpdateState is the full persisted state, written atomically to a JSON file.

func EmptyState

func EmptyState() UpdateState

EmptyState returns the initial state for a fresh install or unreadable file.

func LoadState

func LoadState(path string) UpdateState

LoadState reads and validates the persisted update state. Any problem (missing file, parse error, schema mismatch) yields a fresh EmptyState so a corrupt file can never wedge the updater.

type Updater

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

Updater orchestrates checking, scheduling, applying and verifying updates.

func New

func New(cfg Config, logger *zap.SugaredLogger, broadcast func(int)) *Updater

New builds an updater. broadcast may be nil.

func (*Updater) Acknowledge

func (u *Updater) Acknowledge() error

Acknowledge clears the terminal rollback-failed state so automatic applies can resume once an admin has resolved the underlying problem. It returns an error if there is nothing to acknowledge.

func (*Updater) ApplyNow

func (u *Updater) ApplyNow() error

ApplyNow manually triggers an apply of the latest known release. It returns an error if policy forbids it or no release is known.

func (*Updater) CheckNow

func (u *Updater) CheckNow()

CheckNow triggers an immediate release check in the background.

func (*Updater) CheckNowSync

func (u *Updater) CheckNowSync() Status

CheckNowSync runs a release check synchronously and returns the new status. Used by the admin UI's "check for updates" action.

func (*Updater) IsAcceptingConnections

func (u *Updater) IsAcceptingConnections() bool

IsAcceptingConnections reports whether new pad connections should be allowed. It is false only while draining for an update.

func (*Updater) MarkBootHealthy

func (u *Updater) MarkBootHealthy()

MarkBootHealthy is called once the server is up and serving. It confirms a pending update as verified and cancels the rollback timer.

func (*Updater) Start

func (u *Updater) Start()

Start detects the install method, loads persisted state, resolves any pending verification from a previous restart, and begins polling.

func (*Updater) Status

func (u *Updater) Status() Status

Status returns the current updater status.

func (*Updater) Stop

func (u *Updater) Stop()

Stop halts polling and any armed timers.

type VersionChecker

type VersionChecker struct {
	HTTPClient *http.Client
	Repo       string // e.g. "ether/etherpad-go"
	// contains filtered or unexported fields
}

VersionChecker polls the GitHub releases API with ETag-based caching.

func NewVersionChecker

func NewVersionChecker(repo string) *VersionChecker

NewVersionChecker builds a checker for the given repo.

func (*VersionChecker) CheckLatest

func (v *VersionChecker) CheckLatest(prevETag string) CheckResult

CheckLatest fetches the latest (non-prerelease) release, sending the previous ETag so an unchanged release returns notmodified cheaply.

Jump to

Keyboard shortcuts

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