sops/version/version.go

230 строки
8.1 KiB
Go

package version
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/blang/semver"
"github.com/hashicorp/go-cleanhttp"
"github.com/urfave/cli"
)
// Version represents the value of the current semantic version.
var Version = "3.9.1"
// PrintVersion prints the current version of sops. If the flag
// `--disable-version-check` is set, the function will not attempt
// to retrieve the latest version from the GitHub API.
//
// If the flag is not set, the function will attempt to retrieve
// the latest version from the GitHub API and compare it to the
// current version. If the latest version is newer, the function
// will print a message to stdout.
func PrintVersion(c *cli.Context) {
out := strings.Builder{}
out.WriteString(fmt.Sprintf("%s %s", c.App.Name, c.App.Version))
if c.Bool("disable-version-check") {
out.WriteString("\n")
} else {
upstreamVersion, upstreamURL, err := RetrieveLatestReleaseVersion()
if err != nil {
out.WriteString(fmt.Sprintf("\n[warning] failed to retrieve latest version from upstream: %v\n", err))
} else {
outdated, err := AIsNewerThanB(upstreamVersion, Version)
if err != nil {
out.WriteString(fmt.Sprintf("\n[warning] failed to compare current version with latest: %v\n", err))
} else {
if outdated {
out.WriteString(fmt.Sprintf("\n[info] a new version of sops (%s) is available, you can update by visiting: %s\n", upstreamVersion, upstreamURL))
} else {
out.WriteString(" (latest)\n")
}
}
}
}
fmt.Fprintf(c.App.Writer, "%s", out.String())
}
// AIsNewerThanB compares two semantic versions and returns true if A is newer
// than B. The function will return an error if either version is not a valid
// semantic version.
func AIsNewerThanB(A, B string) (bool, error) {
if strings.HasPrefix(B, "1.") {
// sops 1.0 doesn't use the semver format, which will
// fail the call to `make` below. Since we now we're
// more recent than 1.X anyway, return true right away
return true, nil
}
// Trim the leading "v" from the version strings, if present.
A, B = strings.TrimPrefix(A, "v"), strings.TrimPrefix(B, "v")
vA, err := semver.Make(A)
if err != nil {
return false, err
}
vB, err := semver.Make(B)
if err != nil {
return false, err
}
if vA.Compare(vB) > 0 {
// vA is newer than vB
return true, nil
}
return false, nil
}
// RetrieveLatestVersionFromUpstream retrieves the most recent release version
// from GitHub. The function returns the latest version as a string, or an
// error if the request fails or the response cannot be parsed.
//
// Deprecated: This function is deprecated in favor of
// RetrieveLatestReleaseVersion, which also provides the URL of the latest
// release.
func RetrieveLatestVersionFromUpstream() (string, error) {
tag, _, err := RetrieveLatestReleaseVersion()
return strings.TrimPrefix(tag, "v"), err
}
// RetrieveLatestReleaseVersion fetches the latest release version from GitHub.
// Returns the latest version as a string and the release URL, or an error if
// the request failed or the response could not be parsed.
//
// The function first attempts redirection-based retrieval (HTTP 301). It's
// preferred over GitHub API due to no rate limiting, but may break on
// redirect changes. If the first attempt fails, it falls back to the GitHub
// API.
//
// Unlike RetrieveLatestVersionFromUpstream, it returns the tag (e.g. "v3.7.3").
func RetrieveLatestReleaseVersion() (tag, url string, err error) {
const repository = "getsops/sops"
return newReleaseFetcher().LatestRelease(repository)
}
// newReleaseFetcher creates and returns a new instance of the releaseFetcher,
// preconfigured with the necessary endpoint information for redirection-based
// and API-based release retrieval.
func newReleaseFetcher() releaseFetcher {
return releaseFetcher{
endpoint: "https://github.com",
apiEndpoint: "https://api.github.com",
}
}
// releaseFetcher is a helper struct used for fetching release information
// from GitHub. It encapsulates the necessary endpoints for redirection-based
// and API-based retrieval methods.
type releaseFetcher struct {
endpoint string
apiEndpoint string
}
// LatestRelease retrieves the most recent release version for a given repository
// by first attempting to fetch it using redirection-based approach. If this
// attempt fails, it then falls back to the versioned GitHub API for retrieval.
//
// It returns the latest version as a string along with its corresponding URL, or
// an error in case both retrieval methods are unsuccessful.
//
// This function combines the advantages of both retrieval strategies: the resilience
// of the redirection-based approach and the reliability of the versioned API usage.
// However, it's worth noting that the API usage can be affected by GitHub's rate limiting.
func (f releaseFetcher) LatestRelease(repository string) (tag, url string, err error) {
if tag, url, err = f.LatestReleaseUsingRedirect(repository); err == nil {
return
}
return f.LatestReleaseUsingAPI(repository)
}
// LatestReleaseUsingRedirect fetches the most recent version of a release
// from the GitHub API. It returns the latest version as a string, along with
// its corresponding URL, or an error in case of a failed request or if the
// response couldn't be parsed.
//
// This method employs a customized HTTP client capable of following HTTP 301
// redirects, which might occur due to repository renaming. It's important to
// note that it does not follow HTTP 302 redirects, the type GitHub employs
// for redirecting to the latest release.
//
// Compared to LatestReleaseUsingAPI, this approach circumvents potential GitHub
// API rate limiting. However, it's worth considering that changes in GitHub's
// redirect handling could potentially disrupt its functionality.
func (f releaseFetcher) LatestReleaseUsingRedirect(repository string) (tag, url string, err error) {
client := cleanhttp.DefaultClient()
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// Follow HTTP 301 redirects, which may be present due to the
// repository being renamed. But do not follow HTTP 302 redirects,
// which is what GitHub uses to redirect to the latest release.
if req.Response.StatusCode == 302 {
return http.ErrUseLastResponse
}
return nil
}
resp, err := client.Head(fmt.Sprintf("%s/%s/releases/latest", f.endpoint, repository))
if err != nil {
return "", "", err
}
if resp.Body != nil {
defer resp.Body.Close()
}
if resp.StatusCode < 300 || resp.StatusCode > 399 {
return "", "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
location := resp.Header.Get("Location")
if location == "" {
return "", "", fmt.Errorf("missing Location header")
}
tagMarker := "releases/tag/"
if tagIndex := strings.Index(location, tagMarker); tagIndex != -1 {
return location[tagIndex+len(tagMarker):], location, nil
}
return "", "", fmt.Errorf("unexpected Location header: %s", location)
}
// LatestReleaseUsingAPI retrieves the most recent release version from the
// GitHub API. It returns the latest version as a string, along with its
// corresponding URL, or an error in case of request failure or parsing issues
// with the response.
//
// This approach boasts higher reliability compared to
// LatestReleaseUsingRedirect as it leverages the versioned GitHub API.
// However, it can be affected by GitHub API rate limiting.
func (f releaseFetcher) LatestReleaseUsingAPI(repository string) (tag, url string, err error) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s/releases/latest", f.apiEndpoint, repository), nil)
if err != nil {
return "", "", err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
res, err := cleanhttp.DefaultClient().Do(req)
if err != nil {
return "", "", fmt.Errorf("GitHub API request failed: %v", err)
}
if res.Body != nil {
defer res.Body.Close()
}
if res.StatusCode != http.StatusOK {
return "", "", fmt.Errorf("GitHub API request failed with status code: %d", res.StatusCode)
}
type release struct {
URL string `json:"html_url"`
Tag string `json:"tag_name"`
}
var m release
if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
return "", "", err
}
return m.Tag, m.URL, nil
}