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
- Variables
- func ExtractBinary(archivePath, dstDir string) (string, error)
- func InstallMethodFrom(path string) string
- func IsCurrent(tag, current string) bool
- func ResolveTarget() (string, error)
- func SigstoreCacheDir() (string, error)
- func StageBeside(target, src string) (string, error)
- func Swap(target, newBinary string) error
- func VerifyChecksum(archivePath, archiveName string, checksums []byte) error
- func VerifySignature(checksums, sig, certPEM []byte, trusted *TrustedMaterial) error
- type Asset
- type Chain
- type GHSource
- type HTTPSource
- type RefusalError
- type Release
- type Runner
- type Source
- type TrustedMaterial
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
IsCurrent reports whether tag names the running version, tolerant of a "v" prefix on either side — the same tolerance version/doctor use.
func ResolveTarget ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 (*Chain) Rungs ¶
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 ¶
NewGHSource builds a GHSource that invokes commands through run.
func (*GHSource) Download ¶
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 ¶
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.
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 Runner ¶
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.