selfupdate

package
v0.5.0-beta.2 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: PostgreSQL Imports: 27 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.

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: it stops a
	// hang, not a slow link.
	DownloadTimeout = 10 * time.Minute

	// DoctorCheckTimeout bounds doctor's whole latest-version check,
	// both rungs together; doctor prints a row and moves on.
	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. The caller sets them, not a Source implementation, so both rungs share one budget.

View Source
const (
	ChecksumsAsset = "checksums.txt"
	BundleAsset    = ChecksumsAsset + ".sigstore.json"
)

The release assets VerifySignature and VerifyChecksum read, as .goreleaser.yaml publishes them.

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 is extracted. That is the path-traversal guard: "foo/pgedge" and "../evil" both fail it. A symlink member, even one named "pgedge", is skipped (tar by its Typeflag, zip by its mode bit), and no Linkname is ever followed or written.

func InstallMethodFrom

func InstallMethodFrom(path string) string

InstallMethodFrom infers the install method from a resolved executable path. `pgedge doctor` and ResolveTarget's refusal checks share it, so the two cannot disagree on how pgedge was installed.

func IsCurrent

func IsCurrent(tag, current string) bool

IsCurrent reports whether tag names the running version, tolerant of a "v" prefix on either side.

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 default, ~/.sigstore/root, is outside ~/.pgedge/cli, the only directory this CLI owns.

func StageBeside

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

StageBeside copies src to "<target>.new", with target's permission bits, and returns that path. Same directory, so the rename that follows is atomic, and on Linux possible at all: renaming from $TMPDIR fails with EXDEV when /tmp is another filesystem, and a tmpfs /tmp with the binary in ~/.local/bin is the common non-root install.

A failure removes whatever was written, so an error leaves no path to clean up.

func Swap

func Swap(target, newBinary string) error

Swap replaces target with newBinary. Outside Windows an executing file can be renamed over, so this is a chmod to target's bits and one os.Rename. StageBeside put newBinary in target's directory, so the rename is atomic and target's path is never empty.

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, bundleJSON []byte, trusted *TrustedMaterial,
) error

VerifySignature proves checksums.txt came from our release workflow, which is what makes the checksums worth comparing against. bundleJSON is the release's `checksums.txt.sigstore.json`, the Sigstore bundle cosign's --bundle writes: certificate, signature, log entry and timestamps in one file, so nothing is fetched here.

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 repository is public, so the unauthenticated HTTP rung serves a user without gh, and gh is tried only when it fails.

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, so a test can pin the production order: unauthenticated HTTP first, so a user without gh never needs it. 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 writes the file itself.

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, so --verbose and --debug show which rung a failure came from. gh's stderr is already in the returned error, so it is not repeated.

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: the repository is public, so the private-asset endpoint an authenticated transport would need is deliberately not built.

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 download client is capped at Verbose: dumping megabytes of binary archive would bury the line --debug was turned on to read.

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 is an error, never an empty list, so Chain falls through to gh rather than reporting nothing to update.

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 passes exec.CommandContext; tests inject a stub that re-execs the test binary, so no test runs a real gh.

The context is the deadline seam: a child process has no timeout of its own, so an unauthenticated `gh` sitting on a prompt would hang the command.

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 the Sigstore roots a release's certificate must chain to, and whose logs and timestamp authorities must have dated and recorded its signature.

func NewTestTrustedMaterial

func NewTestTrustedMaterial(roots root.TrustedMaterial) *TrustedMaterial

NewTestTrustedMaterial builds a TrustedMaterial from a caller-minted Sigstore root (e.g. sigstore-go's ca.NewVirtualSigstore), for tests outside this package that drive VerifySignature end to end without dialing public Sigstore. SCT verification is skipped: 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.

Jump to

Keyboard shortcuts

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