update

package
v0.1.0-alpha.1 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Overview

Package update finds, downloads and verifies new releases of Regulus. The hub drives it (see the design in the README): it asks the release source what is newest on the chosen channel and keeps a verified copy in a local cache, ready for whoever applies it.

Index

Constants

View Source
const (
	// OutcomeInstalling is written when the helper starts and replaced when it
	// ends.
	OutcomeInstalling = "installing"
	OutcomeApplied    = "applied"
	// OutcomeRolledBack: the new version was installed but did not come up
	// healthy, so the previous one was put back.
	OutcomeRolledBack = "rolled_back"
	// OutcomeFailed: nothing was changed.
	OutcomeFailed = "failed"
)

Outcomes of an installation.

View Source
const (
	FileChecksums = checksumsName
	FileBundle    = bundleName
)

Names of the files of a release, for Fetcher.

View Source
const (
	EventSettings       = "update_settings_changed"
	EventDownloaded     = "update_downloaded"
	EventDownloadFailed = "update_download_failed"
	EventApplyRequested = "update_apply_requested"
	EventApplied        = "update_applied"
	EventRolledBack     = "update_rolled_back"
	EventApplyFailed    = "update_apply_failed"
)

Audit events recorded by the manager.

View Source
const ConnectedName = "connected.json"

ConnectedName is the file an agent writes when the hub accepted it.

View Source
const (

	// DefaultCheckEvery is how often the hub asks the release server.
	DefaultCheckEvery = 6 * time.Hour
)
View Source
const SystemActor = "system"

SystemActor is the actor of what the hub does by itself.

View Source
const (
	// VerifiedChecksum: the archive matches the SHA-256 in the release's
	// checksums.txt. That catches a corrupted or truncated download, but both
	// files come from the same place, so it does not prove who made them.
	// A Stager without a Verifier stops here.
	VerifiedChecksum = "checksum"
)

How far a staged release was checked.

View Source
const VerifiedSignature = "signature"

VerifiedSignature: the checksums file carries a valid Sigstore signature made by the release workflow of this repository for this very tag, and the archive matches it.

Variables

View Source
var (
	// ErrInvalidSettings wraps the explanation of settings that cannot be saved.
	ErrInvalidSettings = errors.New("invalid update settings")

	ErrChecksOff       = errors.New("update checks are turned off (updates.check in the configuration)")
	ErrBusy            = errors.New("an update check or download is already running")
	ErrTooSoon         = errors.New("the release server was asked a moment ago; wait a little before asking again")
	ErrNothingToFetch  = errors.New("there is no newer release to download")
	ErrNotReleaseBuild = errors.New("this build is not a release, so it cannot tell whether a release is newer")

	ErrApplyDisabled  = errors.New("installing updates from the dashboard is turned off (updates.apply); it needs the regulus-update.path unit the installer sets up")
	ErrNothingToApply = errors.New("there is no downloaded update to install")
	ErrNotVerified    = errors.New("the downloaded update was not verified by its signature, so it will not be installed")
	ErrApplying       = errors.New("an update is already being installed")
)

Errors returned by the manager.

View Source
var ErrNoRequest = errors.New("there is no update request")

ErrNoRequest is returned by the installer when the hub left nothing to do.

Functions

func ApplyDir

func ApplyDir(dataDir string) string

ApplyDir is the directory, inside the hub's data directory, that the hub and the helper share. systemd watches the request file in it.

func ArchiveName

func ArchiveName(v Version, goos, goarch string) string

ArchiveName is the name GoReleaser gives the archive of a platform.

func IsUpstream

func IsUpstream(err error) bool

IsUpstream reports whether err is a failure of the release server or of the way to it, not of the hub.

func WriteConnected

func WriteConnected(dataDir string, c Connected) error

WriteConnected records that the agent of dataDir is connected.

func WriteRequest

func WriteRequest(dir string, req ApplyRequest) error

WriteRequest leaves a request for the helper. The file appears all at once.

func WriteResult

func WriteResult(dir string, res ApplyResult) error

WriteResult records the outcome. It is readable by the hub's user.

Types

type ApplyRequest

type ApplyRequest struct {
	Version     string    `json:"version"`
	RequestedBy string    `json:"requested_by"`
	RequestedAt time.Time `json:"requested_at"`
}

ApplyRequest asks the helper to install a staged release.

func ReadRequest

func ReadRequest(dir string) (ApplyRequest, bool)

ReadRequest returns the request waiting for the helper, if any.

type ApplyResult

type ApplyResult struct {
	Version     string    `json:"version"`
	From        string    `json:"from,omitempty"`
	Outcome     string    `json:"outcome"`
	Message     string    `json:"message,omitempty"`
	RequestedBy string    `json:"requested_by,omitempty"`
	At          time.Time `json:"at"`
}

ApplyResult is what the helper reports.

func ReadResult

func ReadResult(dir string) (ApplyResult, bool)

ReadResult returns the last outcome the helper wrote, if any.

type Asset

type Asset struct {
	Name string
	URL  string
	Size int64
}

Asset is a file attached to a release.

type Auditor

type Auditor interface {
	Record(ctx context.Context, actor, event, target, detail string)
}

Auditor records events; *auth.Service implements it.

type Channel

type Channel string

Channel decides which releases are offered.

const (
	// Stable offers releases only.
	Stable Channel = "stable"
	// Beta offers releases and beta pre-releases, whichever is newest.
	Beta Channel = "beta"
	// Alpha offers every release, alpha pre-releases included.
	Alpha Channel = "alpha"
)

func (Channel) Offers

func (c Channel) Offers(v Version) bool

Offers reports whether a release of version v is offered on channel c.

func (Channel) Valid

func (c Channel) Valid() bool

Valid reports whether c is a known channel.

type Compat

type Compat string

Compat tells how the release of an agent relates to the hub's.

const (
	// CompatCurrent: the same release.
	CompatCurrent Compat = "current"
	// CompatBehind: older than the hub but still supported (the previous
	// minor release, or any release of the previous major). It connects; the
	// dashboard suggests updating it.
	CompatBehind Compat = "behind"
	// CompatAhead: newer than the hub. The hub is updated first, so this
	// means the hub is the one to update. It connects if the protocol allows.
	CompatAhead Compat = "ahead"
	// CompatTooOld: further behind than the hub supports. It is refused.
	CompatTooOld Compat = "too_old"
	// CompatUnknown: one of the two is not a release build (a development
	// build, or a version that cannot be read). It connects.
	CompatUnknown Compat = "unknown"
)

func CheckCompat

func CheckCompat(hubVersion, agentVersion string) Compat

CheckCompat applies the rule "the hub accepts agents of its own release series and the previous one": the same major and minor, one minor behind, or any release of the previous major. The protocol version is negotiated separately and can still refuse an agent this accepts.

type Connected

type Connected struct {
	Version string    `json:"version"`
	At      time.Time `json:"at"`
}

Connected is written by an agent each time the hub accepts it. After an update the helper reads it: an agent that connected again with the new version is a healthy one.

func ReadConnected

func ReadConnected(dataDir string) (Connected, bool)

ReadConnected returns what the agent of dataDir last wrote.

type Fetcher

type Fetcher func(ctx context.Context, file string, w io.Writer) error

Fetcher writes one file of a release to w, from wherever the release is offered. file is FileChecksums, FileBundle or the name of the archive.

type GitHubSource

type GitHubSource struct {
	// Repo is "owner/name".
	Repo string
	// BaseURL is the API address; empty means https://api.github.com.
	BaseURL string
	// UserAgent identifies the hub to GitHub.
	UserAgent string
	Client    *http.Client
	// contains filtered or unexported fields
}

GitHubSource reads the releases of a repository from the GitHub API. The repository is public, so no token is needed; the unauthenticated limit (60 requests an hour per address) is respected with conditional requests and by the caller checking only every few hours.

func (*GitHubSource) Releases

func (g *GitHubSource) Releases(ctx context.Context) ([]Release, error)

Releases fetches the most recent releases.

type Info

type Info struct {
	Version     string    `json:"version"`
	Tag         string    `json:"tag"`
	Prerelease  bool      `json:"prerelease"`
	Channel     Channel   `json:"channel"`
	URL         string    `json:"url"`
	Notes       string    `json:"notes,omitempty"`
	PublishedAt time.Time `json:"published_at"`
}

Info is a release as shown to the administrator.

type Installer

type Installer struct {
	// DataDir is the hub's data directory; the staged release and the request
	// are in it.
	DataDir string
	// StateDir belongs to root: the copy of the previous binary and the cache
	// of the signature trust root.
	StateDir string
	// Binary is the executable to replace, for example /usr/bin/regulus.
	Binary string
	// Unit is the service to restart, for example regulus.service.
	Unit string

	Verifier Verifier
	Run      Runner
	// Agent is true on a server that runs the agent: its health is that it
	// connected to the hub again (see WriteConnected), not the hub's local
	// status socket.
	Agent bool
	// Probe returns the version of the service that is running, or an error
	// while it is not up. Only a process that started at or after since counts:
	// the one that was there before the restart does not.
	Probe func(ctx context.Context, since time.Time) (string, error)

	// HealthTimeout is how long the new version has to come up; it must then
	// keep running for Stable. Poll is how often it is looked at.
	HealthTimeout, Stable, Poll time.Duration

	GOOS, GOARCH string
	Now          func() time.Time
	Log          *slog.Logger
}

Installer is the helper that runs as root. Nothing it is given by the hub's user is trusted: the request only says which version, and the archive is checked again here, signature included, before a byte of it is installed.

func (*Installer) Apply

func (in *Installer) Apply(ctx context.Context) (ApplyResult, error)

Apply installs the staged release the request asks for. The returned result has also been written for the hub. The error is the same as Message and is returned for the exit status.

type Manager

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

Manager looks for new releases and keeps the newest one downloaded. It does not apply anything.

func New

func New(opts Options) *Manager

New returns a Manager. Call Run to start the periodic checks.

func (*Manager) Apply

func (m *Manager) Apply(ctx context.Context, by string) (Status, error)

Apply asks the helper that runs as root to install the downloaded release, and returns at once. The hub restarts when the helper replaces it; the outcome is in Status afterwards, and in the audit log.

func (*Manager) Check

func (m *Manager) Check(ctx context.Context) (Status, error)

Check asks the release server now, for an administrator who pressed the button. It refuses to do so more than once in a short while.

func (*Manager) Close

func (m *Manager) Close()

Close waits for a download in progress to end. Cancel the context given to Run first.

func (*Manager) Download

func (m *Manager) Download(ctx context.Context, by string) (Status, error)

Download starts downloading the newest release on the channel in the background and returns at once; Status shows the progress. The download is checked against the release's checksums before it is kept.

func (*Manager) EnsureRelease

func (m *Manager) EnsureRelease(ctx context.Context, version string, plats []Platform) error

EnsureRelease makes sure the cache holds the release of this very version, signature checked, with the archive of every platform given, so the hub can serve it to the agents it manages. It downloads what is missing.

func (*Manager) Files

func (m *Manager) Files() *Stager

Files serves the cached release to the agents.

func (*Manager) Run

func (m *Manager) Run(ctx context.Context)

Run checks for updates now and then until ctx ends. It returns at once when checks are off.

func (*Manager) SetSettings

func (m *Manager) SetSettings(ctx context.Context, by string, s Settings) (Status, error)

SetSettings saves the choices. Changing the channel makes the last answer stale, so a new check starts at once.

func (*Manager) Settings

func (m *Manager) Settings(ctx context.Context) (Settings, error)

Settings returns the saved choices, with the defaults for what was never chosen: manual mode, and the channel of the running build (a hub that runs a beta follows the beta channel, one that runs an alpha the alpha channel).

func (*Manager) Status

func (m *Manager) Status(ctx context.Context) (Status, error)

Status reports the current state.

type Mode

type Mode string

Mode decides what the hub does when a newer release exists.

const (
	// ModeManual only tells the administrator that a new version exists.
	ModeManual Mode = "manual"
	// ModeDownload also downloads and checks it, then waits to be told to
	// apply it.
	ModeDownload Mode = "download"
	// ModeAutomatic also installs it, by itself, inside the maintenance
	// window. A version that was rolled back is not tried again.
	ModeAutomatic Mode = "automatic"
)

type Options

type Options struct {
	DB     *sql.DB
	Log    *slog.Logger
	Source Source
	Stager *Stager
	Audit  Auditor
	// Version is the version the binary was built with.
	Version      string
	GOOS, GOARCH string
	// ApplyDir is the directory shared with the root helper (see ApplyDir);
	// empty when installing from the dashboard is turned off.
	ApplyDir string
	// Enabled is false when the hub must not contact the release server.
	Enabled    bool
	CheckEvery time.Duration
	// Now may be replaced in tests.
	Now func() time.Time
}

Options configure a Manager.

type Platform

type Platform struct{ OS, Arch string }

Platform is an operating system and architecture a release is built for.

type Release

type Release struct {
	Version     Version
	Tag         string
	URL         string // page of the release, for the release notes
	Notes       string
	PublishedAt time.Time
	Assets      []Asset
}

Release is a published version of the core.

func Latest

func Latest(releases []Release, channel Channel) (best Release, ok bool)

Latest returns the newest release on the channel, or ok = false when there is none.

func (Release) Asset

func (r Release) Asset(name string) (Asset, bool)

Asset returns the asset with the given name.

type Runner

type Runner func(ctx context.Context, name string, args ...string) ([]byte, error)

Runner runs a command and returns what it printed.

type Settings

type Settings struct {
	Mode    Mode    `json:"mode"`
	Channel Channel `json:"channel"`
	// WindowStart and WindowEnd ("15:04", the hub's local time) bound when the
	// automatic mode may install. They may cross midnight.
	WindowStart string `json:"window_start,omitempty"`
	WindowEnd   string `json:"window_end,omitempty"`
}

Settings are the update choices an administrator makes in the dashboard.

func (Settings) InWindow

func (s Settings) InWindow(now time.Time) bool

InWindow reports whether the time falls inside the maintenance window.

func (Settings) Validate

func (s Settings) Validate() error

Validate reports whether the settings can be saved.

type SigstoreVerifier

type SigstoreVerifier struct {
	// Trust provides the trusted root; nil uses PublicTrust with no cache.
	Trust TrustSource
	// Signer is the expected identity (the certificate's subject alternative
	// name) for a tag; nil expects the release workflow of this repository.
	Signer func(tag string) string
	// Issuer is the expected OIDC issuer; empty means GitHub Actions.
	Issuer string
	// contains filtered or unexported fields
}

SigstoreVerifier verifies bundles made by cosign's keyless signing.

func (*SigstoreVerifier) Verify

func (v *SigstoreVerifier) Verify(ctx context.Context, tag string, checksums, bundleJSON []byte) error

Verify implements Verifier.

type Source

type Source interface {
	Releases(ctx context.Context) ([]Release, error)
}

Source lists the releases of the core, in any order.

type Staged

type Staged struct {
	Version  string    `json:"version"`
	Tag      string    `json:"tag"`
	Archive  string    `json:"archive"`
	SHA256   string    `json:"sha256"`
	Size     int64     `json:"size"`
	Verified string    `json:"verified"`
	StagedAt time.Time `json:"staged_at"`
	// Signed is true when the signature bundle of checksums.txt was published
	// and kept next to it. Verified says whether it was checked.
	Signed bool `json:"signed"`
}

Staged describes a downloaded release kept in the cache.

func (Staged) Path

func (s Staged) Path(dir, name string) string

Path returns where a file of the staged release is, under the cache dir.

type Stager

type Stager struct {
	// Dir is the cache; it is created when needed.
	Dir    string
	Client *http.Client
	Now    func() time.Time
	// Verifier, if set, checks the signature of checksums.txt before anything
	// else is downloaded; a release without a valid signature is refused.
	Verifier Verifier
	// Keep, if set, is a version that is never pruned: the one this server runs,
	// whose files the hub serves to the agents it manages.
	Keep string
}

Stager downloads releases into a cache directory.

func (*Stager) Current

func (s *Stager) Current() (Staged, bool)

Current returns the staged release in the cache, if there is one and its files are still intact.

func (*Stager) HasArchives

func (s *Stager) HasArchives(version string, plats ...Platform) bool

HasArchives reports whether the staged release of a version holds the archive of every platform given, and has its signature bundle.

func (*Stager) Offer

func (s *Stager) Offer(version string, p Platform) (name, sha string, size int64, err error)

Offer describes the archive of a platform in the staged release of a version, from the signed checksum list: its name, SHA-256 and size.

func (*Stager) Open

func (s *Stager) Open(version, name string) (*os.File, int64, error)

Open opens a file of a staged release so it can be served to an agent. The name must be the checksum list, the signature bundle or an archive of that release; nothing else in the cache is reachable.

func (*Stager) Stage

func (s *Stager) Stage(ctx context.Context, rel Release, goos, goarch string) (Staged, error)

Stage downloads the archive of the release for a platform, checks it against the release's checksums.txt and keeps it, with the checksums and the signature bundle, in Dir/<version>/. Any other staged version is removed. Nothing reaches the final place unless the checksum matches.

func (*Stager) StageFetched

func (s *Stager) StageFetched(ctx context.Context, v Version, plat Platform, wantSHA string, fetch Fetcher) (Staged, error)

StageFetched keeps a release that was offered by a hub rather than found on GitHub: the files are fetched, the archive is compared with the checksum list (and with wantSHA, the value the hub named), and the signature of the list is verified when the Stager has a Verifier. The result is laid out like Stage's, so the helper that runs as root installs it the same way.

func (*Stager) StagePlatforms

func (s *Stager) StagePlatforms(ctx context.Context, rel Release, plats ...Platform) (Staged, error)

StagePlatforms is Stage for several platforms at once: the hub also serves the archives of the servers it manages. The first platform is the one described by the returned Staged.

func (*Stager) Verify

func (s *Stager) Verify(st Staged) error

Verify hashes the staged archive again and compares it with the recorded checksum, so a file changed on disk since the download is noticed.

type Status

type Status struct {
	// CurrentVersion is the version of this hub as it was built.
	CurrentVersion string `json:"current_version"`
	// ReleaseBuild is false for a development build, which cannot compare
	// itself with releases: no update is ever reported for it.
	ReleaseBuild bool `json:"release_build"`
	// ChecksEnabled is false when updates.check is off: the hub never contacts
	// the release server.
	ChecksEnabled bool    `json:"checks_enabled"`
	Mode          Mode    `json:"mode"`
	Channel       Channel `json:"channel"`
	WindowStart   string  `json:"window_start,omitempty"`
	WindowEnd     string  `json:"window_end,omitempty"`
	// Platform is the build of the release this hub would download.
	Platform string `json:"platform"`

	CheckedAt  *time.Time `json:"checked_at"`
	CheckError string     `json:"check_error,omitempty"`
	Checking   bool       `json:"checking"`
	// Latest is the newest release on the channel at the last check.
	Latest          *Info `json:"latest"`
	UpdateAvailable bool  `json:"update_available"`

	// Staged is the downloaded and checked release waiting in the cache.
	Staged        *Staged `json:"staged"`
	Downloading   bool    `json:"downloading"`
	DownloadError string  `json:"download_error,omitempty"`

	// ApplyEnabled is true when this hub can have an update installed (the
	// helper is set up; see updates.apply). ApplyAvailable adds that a
	// verified release is waiting. Applying is true from the request until the
	// helper has finished, and ApplyStalled when nothing picked the request up.
	ApplyEnabled   bool `json:"apply_enabled"`
	ApplyAvailable bool `json:"apply_available"`
	Applying       bool `json:"applying"`
	ApplyStalled   bool `json:"apply_stalled,omitempty"`
	// LastApply is the outcome of the last installation.
	LastApply *ApplyResult `json:"last_apply"`
}

Status is what the hub knows about updates, as the API reports it.

type TrustSource

type TrustSource func(ctx context.Context) (root.TrustedMaterial, error)

TrustSource provides the Sigstore trusted material.

func PublicTrust

func PublicTrust(cacheDir string) TrustSource

PublicTrust returns the trust source for the public Sigstore instance. It asks the Sigstore TUF repository for the current trusted root (so a key rotation reaches a hub that nobody updated), keeping the answer in cacheDir (empty: not cached on disk), and falls back to the snapshot embedded in the binary when the repository cannot be reached.

type Verifier

type Verifier interface {
	// Verify checks the Sigstore bundle of checksums for the release at tag.
	Verify(ctx context.Context, tag string, checksums, bundleJSON []byte) error
}

Verifier checks the signature of a release's checksums.txt.

type Version

type Version struct {
	Major, Minor, Patch uint64
	// Pre holds the dot-separated pre-release identifiers, such as ["beta", "1"].
	Pre []string
}

Version is a SemVer 2.0 version.

func ParseBuildVersion

func ParseBuildVersion(s string) (v Version, ok bool)

ParseBuildVersion reads the version a binary was built with (-ldflags "-X main.version=..."). It reports ok = false for builds that are not a release: "dev", or the output of `git describe` for a commit after a tag. Such a build cannot tell whether a release is newer.

func ParseVersion

func ParseVersion(s string) (Version, error)

ParseVersion reads a version such as "1.4.0", "v1.4.0" or "v1.5.0-beta.1". Build metadata (+...) is accepted and ignored, as SemVer says it must be when comparing.

func (Version) Channel

func (v Version) Channel() Channel

Channel is the channel a release of this version belongs to: Stable for a release, Beta for a pre-release named beta or rc (1.5.0-beta.1, 1.5.0-rc.1), and Alpha for any other pre-release (1.5.0-alpha.1), so an unknown kind is never offered to more cautious channels.

func (Version) Compare

func (v Version) Compare(o Version) int

Compare returns -1, 0 or 1 as v is older than, equal to or newer than o, following SemVer precedence: a pre-release is older than its release.

func (Version) Prerelease

func (v Version) Prerelease() bool

Prerelease reports whether the version is a pre-release (beta or alpha).

func (Version) String

func (v Version) String() string

String returns the version without a leading "v".

Jump to

Keyboard shortcuts

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