selfupdate

package
v0.5.0-beta.1 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: PostgreSQL Imports: 34 Imported by: 0

Documentation

Overview

Package selfupdate implements `pgedge self update`: resolving a release tag from a fetched release list, verifying the downloaded asset, and swapping it into place. This file holds the shapes later tasks' Source ladder, verification and swap steps build on.

Index

Constants

View Source
const (
	// ListTimeout bounds a release-list fetch made by `self update`.
	ListTimeout = 30 * time.Second

	// DownloadTimeout bounds one asset download. Generous on purpose:
	// a release archive on a slow link is legitimately slow, and the
	// deadline is here to stop a hang, not to police throughput.
	DownloadTimeout = 10 * time.Minute

	// DoctorCheckTimeout bounds doctor's whole latest-version check —
	// both rungs together. doctor prints a row and moves on, so it
	// gives up far sooner than an update would.
	DoctorCheckTimeout = 5 * time.Second

	// ProbeTimeout bounds the post-swap `pgedge version -o json` exec.
	ProbeTimeout = 30 * time.Second
)

The deadlines every Source call runs under. They live here, beside the interface, because a Source implementation must not invent its own budget: the caller decides how long the whole ladder may take, and both rungs share it.

A gh rung execs a child process, which has no timeout of its own — an unauthenticated `gh` waiting on a prompt would otherwise hang the command forever.

Variables

View Source
var ErrGHUnauthenticated = errors.New("gh is not authenticated")

ErrGHUnauthenticated marks a gh failure classified as an unauthenticated session (gh auth status itself failing after a fetch failed), distinct from any other reason gh might fail. The caller uses errors.Is to pick exit 5 over the generic exit 1.

Functions

func ExtractBinary

func ExtractBinary(archivePath, dstDir string) (string, error)

ExtractBinary pulls the pgedge binary out of the archive at archivePath into dstDir and returns the extracted file's path.

Only the EXACT top-level member name ("pgedge", or "pgedge.exe" in a zip) is ever extracted; every other entry is skipped. That is the path-traversal guard: a nested path ("foo/pgedge") and a name containing ".." ("../evil") both fail the exact-match test. A symlink member, even one literally named "pgedge", is rejected separately in each format — tar by its Typeflag, zip by the mode bit its entries carry instead. No entry's Linkname is ever followed or written.

func InstallMethodFrom

func InstallMethodFrom(path string) string

InstallMethodFrom infers the install method from a resolved executable path. Moved here verbatim from internal/cli/doctor.go's installMethodFrom (same cases, same returns) so `pgedge doctor` and the refusal checks in ResolveTarget share one definition of "how was this installed" instead of two that could drift apart.

func IsCurrent

func IsCurrent(tag, current string) bool

IsCurrent reports whether tag names the running version, tolerant of a "v" prefix on either side — the same tolerance version/doctor use.

func ResolveTarget

func ResolveTarget() (string, error)

ResolveTarget locates the running pgedge binary — os.Executable, resolved through any symlink — and applies the refusal checks before handing it back as a swap target.

func SigstoreCacheDir

func SigstoreCacheDir() (string, error)

SigstoreCacheDir is where the refreshed TUF trust root is cached.

sigstore-go's own default is ~/.sigstore/root, which this CLI has no business writing: ~/.pgedge/cli is the only directory it owns (#393), and a directory a user has never heard of appearing in their home is a surprise a verification step should not spring.

func StageBeside

func StageBeside(target, src string) (string, error)

StageBeside copies src to "<target>.new" and returns that path, with target's own permission bits.

Same directory, deliberately: that is what makes the following rename atomic, and on Linux it is what makes it POSSIBLE. Staging under $TMPDIR and renaming from there fails with EXDEV whenever /tmp is a different filesystem from the install directory — a tmpfs /tmp with the binary in ~/.local/bin is the modal non-root install, so this is the common case rather than an exotic one.

A failure leaves nothing behind: whatever was written is removed before the error returns, so a caller that got an error has no path to clean up.

func Swap

func Swap(target, newBinary string) error

Swap replaces target with newBinary. On every platform but Windows, an executing file can be renamed out from under itself, so this is a chmod (newBinary takes target's existing permission bits) then a single os.Rename onto target. newBinary comes from StageBeside, so it is already in target's own directory: same filesystem, so the rename is atomic and there is never a moment with no binary at target's path.

func VerifyChecksum

func VerifyChecksum(archivePath, archiveName string, checksums []byte) error

VerifyChecksum guarantees the bytes on disk at archivePath are the bytes checksums.txt recorded for archiveName — so an archive swapped in transit, or a truncated download, cannot reach the extractor. It is the second half of the trust chain: VerifySignature proves checksums.txt is ours, this proves the archive matches it.

The match is on the FILENAME FIELD, never a substring: checksums.txt lists each archive twice over, once as `<name>` and once as `<name>.sbom.json`, and a substring scan takes whichever came first.

func VerifySignature

func VerifySignature(
	checksums, sig, certPEM []byte, trusted *TrustedMaterial,
) error

VerifySignature proves checksums.txt came from our release workflow, which is what makes the checksums worth comparing against. sig and certPEM are the release's `checksums.txt.sig` and `checksums.txt.pem` as cosign wrote them (base64 over the DER signature and over the PEM certificate respectively).

Failure is final: there is no checksum-only fallback, because a `self update` running unattended has no operator to read a warning.

Types

type Asset

type Asset struct {
	Name string
}

Asset is one downloadable file attached to a release.

type Chain

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

Chain tries primary, falling back to fallback when primary fails. Production wires HTTPSource as primary and GHSource as fallback (the design's "ladder"): the http rung 404s while pgEdge/pgedge-cli stays private, so every fetch rides gh, and the day the repo goes public the http rung starts succeeding with no code change on either side.

func NewChain

func NewChain(primary, fallback Source) *Chain

NewChain builds a Chain trying primary first, then fallback.

func (*Chain) Download

func (c *Chain) Download(
	ctx context.Context, tag, assetName, dstDir string,
) (string, error)

func (*Chain) Releases

func (c *Chain) Releases(ctx context.Context) ([]Release, error)

func (*Chain) Rungs

func (c *Chain) Rungs() []Source

Rungs reports the chain's sources in the order they are tried. It exists so a test can assert the PRODUCTION ladder's order — unauthenticated HTTP first, gh second — which is the property the public flip rests on: the first rung starting to succeed is what makes the gh dependency evaporate with no code change. Reversed, every other test still passes.

type GHSource

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

GHSource fetches releases and downloads assets by shelling out to the gh CLI — the ladder's fallback rung, used when the unauthenticated HTTP rung fails.

func NewGHSource

func NewGHSource(run Runner) *GHSource

NewGHSource builds a GHSource that invokes commands through run.

func (*GHSource) Download

func (s *GHSource) Download(
	ctx context.Context, tag, assetName, dstDir string,
) (string, error)

Download execs `gh release download <tag> --repo pgEdge/pgedge-cli --pattern <assetName> --dir <dstDir>`; gh itself writes the file named assetName into dstDir, so success returns that path without this package touching the filesystem.

func (*GHSource) Logging

func (s *GHSource) Logging(out io.Writer, lvl httplog.Level) *GHSource

Logging renders each gh invocation to out at Verbose or above — the command line, then its exit status and elapsed time — so --verbose and --debug show which rung a failure came from (#402). gh's own stderr is already carried in the returned error, so it is not repeated here. It returns s for chaining.

func (*GHSource) Releases

func (s *GHSource) Releases(ctx context.Context) ([]Release, error)

Releases execs `gh api repos/pgEdge/pgedge-cli/releases?per_page=30`, which returns the same JSON shape as the plain REST API.

type HTTPSource

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

HTTPSource fetches releases and assets from GitHub's unauthenticated REST API — the ladder's first rung. It carries no credentials: an authenticated transport would need the private-asset endpoint dance, and that need disappears the day the repo goes public, so it is deliberately not built (see the design spec).

func NewHTTPSource

func NewHTTPSource(baseAPI, baseDL string) *HTTPSource

NewHTTPSource builds an HTTPSource against baseAPI (a GitHub API origin, e.g. "https://api.github.com") and baseDL (a download origin, e.g. "https://github.com"). Splitting them lets tests point each at its own httptest server.

func (*HTTPSource) Download

func (s *HTTPSource) Download(
	ctx context.Context, tag, assetName, dstDir string,
) (string, error)

Download GETs <baseDL>/pgEdge/pgedge-cli/releases/download/<tag>/<assetName> and writes the body to dstDir/<assetName>, returning that path.

func (*HTTPSource) Logging

func (s *HTTPSource) Logging(out io.Writer, lvl httplog.Level) *HTTPSource

Logging renders this rung's traffic to out at lvl, the way every API client in this CLI answers --verbose and --debug (#402). The download client is capped at Verbose whatever lvl says: a release archive is megabytes of binary, and dumping it would bury the one line --debug was turned on to read. It returns s for chaining.

func (*HTTPSource) Releases

func (s *HTTPSource) Releases(ctx context.Context) ([]Release, error)

Releases GETs <baseAPI>/repos/pgEdge/pgedge-cli/releases?per_page=30. A 404 reads as "repository not found or private" — the release list may simply be behind auth, never "no releases exist" — and the Chain relies on that distinction to fall through to gh rather than reporting a real "nothing to update" state.

type RefusalError

type RefusalError struct{ Msg string }

RefusalError marks a target selfupdate must not touch. `self update` only ever manages a binary it fully owns in place; a Homebrew install and a copy inside a git working tree are each owned by something else instead, so ResolveTarget refuses rather than swapping over either.

func (*RefusalError) Error

func (e *RefusalError) Error() string

type Release

type Release struct {
	TagName string
	Assets  []Asset
}

Release is one GitHub (or mirror) release: a tag and its assets.

func Resolve

func Resolve(releases []Release, want string) (Release, error)

Resolve picks the release for want ("" => newest by semver, pre-releases included; else exact tag, v-prefix tolerant). A release whose tag is not valid semver is skipped when ranking, not treated as an error.

type Runner

type Runner func(ctx context.Context, name string, args ...string) *exec.Cmd

Runner constructs the *exec.Cmd for one invocation of an external command. Production code passes exec.CommandContext; tests inject a stub that re-execs the test binary instead of the real gh, so no test ever shells out to a real gh.

The context is the deadline seam: a child process has no timeout of its own, so without it an unauthenticated `gh` sitting on a prompt hangs the command with nothing to stop it.

type Source

type Source interface {
	Releases(ctx context.Context) ([]Release, error)
	Download(ctx context.Context, tag, assetName, dstDir string) (string, error)
}

Source fetches the release list and downloads a chosen asset. HTTPSource, GHSource and Chain all implement it.

type TrustedMaterial

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

TrustedMaterial is everything VerifySignature needs beyond the files a release ships: the Sigstore roots the certificate must chain to, and the transparency-log lookup that dates it.

The log lookup belongs here rather than inside VerifySignature because it is the other half of the same trust decision — swapping in a test CA is only hermetic if the log it consults is swapped with it.

func NewTestTrustedMaterial

func NewTestTrustedMaterial(
	roots root.TrustedMaterial, entries []*tlog.Entry,
) *TrustedMaterial

NewTestTrustedMaterial builds a TrustedMaterial from a caller-minted Sigstore root (e.g. sigstore-go's ca.NewVirtualSigstore) and a fixed transparency-log answer, for command-level tests outside this package that need to drive VerifySignature end to end without dialing public Sigstore infrastructure — every in-package fixture does the same thing with the unexported literal directly; this is that literal's only door for a caller that cannot reach it.

SCT verification is skipped, matching every such fixture: an ephemeral CA embeds no SCT.

func ProductionTrustedMaterial

func ProductionTrustedMaterial() (*TrustedMaterial, error)

ProductionTrustedMaterial loads the public-good Sigstore trust root (sigstore-go's embedded TUF root, refreshed from the CDN and cached under SigstoreCacheDir) and points the log lookup at public Rekor.

Jump to

Keyboard shortcuts

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