Documentation
¶
Overview ¶
Package selfupdate installs a newer build of the running program from a GitHub release, then hands the process back to its supervisor.
It is built for a service that ships as a tarball containing an executable and some directories beside it (a built frontend, say), installed under a systemd unit with Restart=always. Installing swaps the files atomically and raises SIGTERM; systemd starts the new version.
The package deliberately does not manage the systemd unit, sudoers rules or directory permissions. A service has no business granting itself privileges, so those stay with whatever installer put the service there. UnitState exists to notice when that wiring has fallen behind the running build.
Index ¶
- Constants
- func Compare(a, b string) int
- func DefaultAssetName(binary, tag string) string
- func InstallDir() (string, error)
- func Newer(candidate, current string) bool
- func Restart(delay time.Duration)
- func Supervised() bool
- type Asset
- type Config
- type Release
- type Result
- type Status
- type UnitState
- type Updater
- func (u *Updater) AssetNameFor(tag string) string
- func (u *Updater) Available(ctx context.Context) (Release, bool, error)
- func (u *Updater) CheckBundle(r io.Reader) error
- func (u *Updater) Install(ctx context.Context, release Release) (Result, error)
- func (u *Updater) InstallLatest(ctx context.Context) (Result, error)
- func (u *Updater) Latest(ctx context.Context) (Release, error)
- func (u *Updater) Status() Status
- func (u *Updater) UnitState() UnitState
Constants ¶
const StaleUnitProblem = "This build expects a different systemd unit from the one installed. " +
"Self-update replaces only the executable and its data directories, never the unit, " +
"the sudoers rules or file permissions: re-run the installer from this release to apply them."
StaleUnitProblem is the advice to show when the installed unit is out of date.
Variables ¶
This section is empty.
Functions ¶
func Compare ¶
Compare orders two semver-ish tags, returning -1, 0 or 1.
Tags may carry a leading "v" and a prerelease or build suffix. The suffix is only a tiebreaker: 1.2.0 outranks 1.2.0-rc1.
func DefaultAssetName ¶
DefaultAssetName matches the convention `make package` produces: <binary>_<tag>_<goos>_<goarch>.tar.gz.
func InstallDir ¶
InstallDir returns the directory holding the running executable, with symlinks resolved so an update replaces the real file rather than a link.
func Newer ¶
Newer reports whether release tag "candidate" is later than "current".
A build with no version ("" or "dev") counts as older than everything, so a developer build offers the newest release instead of claiming to be current.
func Restart ¶
Restart hands the process back to its supervisor after the given delay.
It raises SIGTERM rather than exiting, so the program's own shutdown runs: for anything holding a database, skipping that risks leaving the write-ahead log unflushed. Call it after the response is on its way out.
It returns immediately; the signal is raised from a goroutine.
Types ¶
type Config ¶
type Config struct {
// Owner and Repo identify the GitHub repository holding the releases.
Owner string
Repo string
// Token authenticates against a private repository. Public repos need none.
Token string
// BinaryName is the executable's name, both inside the release tarball and
// in the install directory.
BinaryName string
// CurrentVersion is the running build's version, normally stamped in with
// -ldflags. "dev" or empty counts as older than any release.
CurrentVersion string
// Dirs are directories in the tarball that replace their counterparts in the
// install directory wholesale. Each value names a file that must exist
// inside, as a sanity check against a half-built bundle:
//
// Dirs: map[string]string{"frontend": "index.html"}
//
// Replacing rather than merging matters: a merge leaves the previous
// release's hashed assets behind forever.
Dirs map[string]string
// AssetName builds the release asset's file name from a tag. It defaults to
// DefaultAssetName, which matches <binary>_<tag>_<goos>_<goarch>.tar.gz.
AssetName func(tag string) string
// InstallDir defaults to the directory holding the running executable.
InstallDir string
// MaxAssetBytes caps the download. Defaults to 512 MiB.
MaxAssetBytes int64
// ExpectedUnit is the systemd unit this build ships, normally embedded with
// go:embed. When set, Status reports whether the installed unit matches.
ExpectedUnit string
// UnitPath defaults to /etc/systemd/system/<BinaryName>.service.
UnitPath string
// APIBase overrides the GitHub API root, for tests.
APIBase string
// UserAgent is sent with every request. Defaults to the binary name.
UserAgent string
// APITimeout and DownloadTimeout default to 30 seconds and 30 minutes.
APITimeout time.Duration
DownloadTimeout time.Duration
// HTTPTransport is shared by both clients when set.
HTTPTransport http.RoundTripper
}
Config describes the program being updated and where its releases live.
type Release ¶
type Release struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
Published string `json:"published_at"`
Assets []Asset `json:"assets"`
}
Release is the subset of a GitHub release this package needs.
type Result ¶
type Result struct {
// Version is the tag that was installed.
Version string
// Asset is the release asset it came from.
Asset string
// Replaced lists the paths swapped in the install directory.
Replaced []string
}
Result describes a completed install.
type Status ¶
type Status struct {
InstallDir string
// Writable is false when the install directory cannot be written.
Writable bool
// WriteProblem explains why, when it cannot.
WriteProblem string
// Supervised is true when systemd started this process, which is what makes
// "exit and come back on the new build" work.
Supervised bool
// Unit reports whether the installed systemd unit matches this build, when
// Config.ExpectedUnit is set.
Unit UnitState
}
Status is what the host allows right now. A UI can use it to explain why updating is unavailable instead of failing when the button is pressed.
func (Status) CanInstall ¶
CanInstall reports whether an update would work here.
type UnitState ¶
type UnitState int
UnitState says whether the installed systemd unit matches this build.
const ( // UnitUnknown means there was nothing to compare: no expected unit // configured, or no unit installed. This is the normal case in development. UnitUnknown UnitState = iota // UnitCurrent means the installed unit matches this build. UnitCurrent // UnitStale means this build expects a different unit from the installed one. UnitStale )
type Updater ¶
type Updater struct {
// contains filtered or unexported fields
}
Updater installs releases of one program.
func (*Updater) AssetNameFor ¶
AssetNameFor is the asset this build looks for in a given release.
func (*Updater) Available ¶
Available reports the latest release and whether it is newer than this build.
func (*Updater) CheckBundle ¶
CheckBundle verifies a release bundle without installing anything.
Worth running in CI against whatever `make package` produced: it is the only thing that catches the packaging and the updater drifting apart, which otherwise shows up for the first time on a device, mid-update.
func (*Updater) InstallLatest ¶
InstallLatest downloads the newest release and swaps it into place.
It does not restart anything: the caller answers its HTTP request or logs a line first, then calls Restart. Nothing is changed on disk unless the whole bundle verifies, and a failure part-way through restores the previous files.
func (*Updater) Latest ¶
Latest returns the repository's latest published release.
GitHub's "latest" endpoint ignores drafts and prereleases, so a release marked prerelease is invisible here by design.
func (*Updater) UnitState ¶
UnitState compares the installed systemd unit with the one this build expects.
Self-update cannot change the unit, because a service that could rewrite its own systemd configuration could grant itself anything. So a release needing new host wiring lands on a machine still running the old unit, and whatever it added silently does not work. Embedding the expected unit and comparing is how that gets noticed rather than debugged.