mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-26 18:24:54 +09:00
823db85509
`go get -u` takes the highest semver the proxy offers: a fork's stray tag sorting above its real branch, or a module that has moved and kept tagging under the old path. Resolving first refuses both, and names the compare link for every version that does move.
278 lines
8.1 KiB
Go
278 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
"golang.org/x/mod/module"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
// proxyBase is the module mirror. It is deliberately the same source the go
|
|
// command already trusts: immutable per version, so the go.mod read back here
|
|
// is the one that will actually be resolved, not whatever a branch tip happens
|
|
// to hold at fetch time.
|
|
// It is a var only so the tests can point it at a fake proxy; nothing changes
|
|
// it at runtime.
|
|
var proxyBase = "https://proxy.golang.org"
|
|
|
|
const proxyTimeout = 30 * time.Second
|
|
|
|
// errNoLockstepSource means the partner version could not be established. The
|
|
// caller must move neither half of the pair: a libc without its sqlite is the
|
|
// exact breakage the lockstep rule exists to prevent.
|
|
var errNoLockstepSource = errors.New("lockstep source unavailable")
|
|
|
|
// errStrayTag means @latest sorts above the pinned version but was committed
|
|
// no later than it.
|
|
var errStrayTag = errors.New("newest tag is older than the pinned version")
|
|
|
|
// errModuleMoved means the newest version under this path declares a different
|
|
// module path.
|
|
var errModuleMoved = errors.New("module has moved to a new path")
|
|
|
|
// fetch performs a GET and returns the body, or an error naming the status.
|
|
func fetch(ctx context.Context, url string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("building request for %s: %w", url, err)
|
|
}
|
|
|
|
client := &http.Client{Timeout: proxyTimeout}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GET %s: %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("GET %s: %w: %s", url, errHTTPStatus, resp.Status)
|
|
}
|
|
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading %s: %w", url, err)
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
var errHTTPStatus = errors.New("unexpected status")
|
|
|
|
// versionInfo is the proxy's @latest and @v/<version>.info response.
|
|
type versionInfo struct {
|
|
Version string
|
|
Time time.Time
|
|
}
|
|
|
|
// fetchInfo reads one of the proxy's info endpoints. ref is "@latest" or
|
|
// "@v/<escaped version>.info".
|
|
func fetchInfo(ctx context.Context, path, ref string) (versionInfo, error) {
|
|
esc, err := module.EscapePath(path)
|
|
if err != nil {
|
|
return versionInfo{}, fmt.Errorf("escaping %s: %w", path, err)
|
|
}
|
|
|
|
body, err := fetch(ctx, proxyBase+"/"+esc+"/"+ref)
|
|
if err != nil {
|
|
return versionInfo{}, err
|
|
}
|
|
|
|
var info versionInfo
|
|
if err := json.Unmarshal(body, &info); err != nil { //nolint:noinlineerr
|
|
return versionInfo{}, fmt.Errorf("decoding %s for %s: %w", ref, path, err)
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// versionTime is when version of path was committed. Pseudo-versions carry it
|
|
// in their own name, so only tagged versions cost a round trip.
|
|
func versionTime(ctx context.Context, path, version string) (time.Time, error) {
|
|
if module.IsPseudoVersion(version) {
|
|
return module.PseudoVersionTime(version)
|
|
}
|
|
|
|
esc, err := module.EscapeVersion(version)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("escaping version %s: %w", version, err)
|
|
}
|
|
|
|
info, err := fetchInfo(ctx, path, "@v/"+esc+".info")
|
|
|
|
return info.Time, err
|
|
}
|
|
|
|
// latestVersion resolves a module's newest usable version through the proxy.
|
|
// current is the version already in use, or "" when the module is not required
|
|
// yet. When nothing newer exists it returns current unchanged.
|
|
//
|
|
// Two things the proxy will hand back that the go command does not protect
|
|
// against, both of which have to be refused rather than committed:
|
|
//
|
|
// - A stray tag. Forks carry tags that sort above the branch the real work
|
|
// happens on, and @latest reports them. Semver says newer; the commit date
|
|
// says otherwise, and the commit date is the one telling the truth.
|
|
// - A module that has moved. Releases keep being tagged under the new path,
|
|
// the proxy still serves them under the old one, and go get then refuses
|
|
// the result with an error that does not say why.
|
|
func latestVersion(ctx context.Context, path, current string) (string, error) {
|
|
info, err := fetchInfo(ctx, path, "@latest")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if info.Version == "" {
|
|
return "", fmt.Errorf("%w: empty @latest for %s", errNoLockstepSource, path)
|
|
}
|
|
|
|
if !semver.IsValid(info.Version) {
|
|
return "", fmt.Errorf("%w: proxy returned %q for %s", errStrayTag, info.Version, path)
|
|
}
|
|
|
|
if current != "" && semver.Compare(info.Version, current) <= 0 {
|
|
return current, nil
|
|
}
|
|
|
|
f, err := modFileOf(ctx, path, info.Version)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if f.Module != nil && f.Module.Mod.Path != path {
|
|
return "", fmt.Errorf("%w: %s@%s declares %s; change the import path by hand",
|
|
errModuleMoved, path, info.Version, f.Module.Mod.Path)
|
|
}
|
|
|
|
if current == "" {
|
|
return info.Version, nil
|
|
}
|
|
|
|
curTime, err := versionTime(ctx, path, current)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !info.Time.After(curTime) {
|
|
return "", fmt.Errorf("%w: %s@%s is dated %s, the pinned %s is dated %s",
|
|
errStrayTag, path, info.Version, info.Time.Format(time.DateOnly),
|
|
current, curTime.Format(time.DateOnly))
|
|
}
|
|
|
|
return info.Version, nil
|
|
}
|
|
|
|
// modFileOf fetches and parses the go.mod of the exact path@version.
|
|
func modFileOf(ctx context.Context, path, version string) (*modfile.File, error) {
|
|
escPath, err := module.EscapePath(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("escaping %s: %w", path, err)
|
|
}
|
|
|
|
escVer, err := module.EscapeVersion(version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("escaping version %s: %w", version, err)
|
|
}
|
|
|
|
body, err := fetch(ctx, proxyBase+"/"+escPath+"/@v/"+escVer+".mod")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f, err := modfile.Parse(path+"@"+version+"/go.mod", body, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing go.mod of %s@%s: %w", path, version, err)
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
// requiredVersion reports the version of dep that f requires.
|
|
func requiredVersion(f *modfile.File, dep string) (string, bool) {
|
|
for _, req := range f.Require {
|
|
if req.Mod.Path == dep {
|
|
return req.Mod.Version, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// partnerVersion resolves the version of dep that owner@ownerVersion pins. It
|
|
// is the whole of the lockstep rule: the partner is never guessed, only read
|
|
// off the owner's own go.mod.
|
|
func partnerVersion(ctx context.Context, owner, ownerVersion, dep string) (string, error) {
|
|
f, err := modFileOf(ctx, owner, ownerVersion)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w: %w", errNoLockstepSource, err)
|
|
}
|
|
|
|
v, ok := requiredVersion(f, dep)
|
|
if !ok {
|
|
return "", fmt.Errorf("%w: %s@%s does not require %s", errNoLockstepSource, owner, ownerVersion, dep)
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// describeChange renders one module's move, as a compare link when the path
|
|
// names a repository a link can be built for. Reviewing fifty version numbers
|
|
// means opening fifty tabs otherwise.
|
|
func describeChange(path, from, to string) string {
|
|
link := compareLink(path, from, to)
|
|
if link == "" {
|
|
return fmt.Sprintf("%s %s -> %s", path, from, to)
|
|
}
|
|
|
|
return fmt.Sprintf("%s %s -> [%s](%s)", path, from, to, link)
|
|
}
|
|
|
|
// compareLink is the GitHub compare URL between two versions of a module, or
|
|
// "" when one cannot be built. The repository is the first three path
|
|
// segments; anything past that is a subdirectory with its own tag prefix.
|
|
func compareLink(path, from, to string) string {
|
|
if !strings.HasPrefix(path, "github.com/") {
|
|
return ""
|
|
}
|
|
|
|
trimmed, _, ok := module.SplitPathVersion(path)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
parts := strings.Split(trimmed, "/")
|
|
if len(parts) < 3 {
|
|
return ""
|
|
}
|
|
|
|
sub := strings.Join(parts[3:], "/")
|
|
|
|
return fmt.Sprintf("https://%s/compare/%s...%s",
|
|
strings.Join(parts[:3], "/"), gitRef(sub, from), gitRef(sub, to))
|
|
}
|
|
|
|
// gitRef is the tag or commit one module version points at. A pseudo-version
|
|
// names a commit; a tagged version inside a subdirectory carries that
|
|
// subdirectory as a prefix.
|
|
func gitRef(sub, version string) string {
|
|
if module.IsPseudoVersion(version) {
|
|
if rev, err := module.PseudoVersionRev(version); err == nil { //nolint:noinlineerr
|
|
return rev
|
|
}
|
|
}
|
|
|
|
if sub != "" {
|
|
return sub + "/" + version
|
|
}
|
|
|
|
return version
|
|
}
|